From 18cf222ea403adee48f0b97fa166e8e68d4bd6e8 Mon Sep 17 00:00:00 2001 From: Nirav Kadiya Date: Fri, 1 Feb 2019 14:44:37 +0530 Subject: [PATCH] [Backport] Fixed - Lifetime update syntax error #13309 (#4) * Escaped output of html attributes * MAGETWO-97466: Refreshing checkout page deletes shipping address on guest checkout * MAGETWO-96711: One page Checkout resets Customer data if Product Qty was changed * MAGETWO-72879: Update product from mini shopping cart doesn't reflect in the shopping cart * meassage icon alignment issue resolved * ENGCOM-3851: Static test fix. * ENGCOM-3893: Static test fix. * MAGETWO-96711: One page Checkout resets Customer data if Product Qty was changed * MAGETWO-97826: Skipped Randomly failing MFTF test AdminCheckingCreditMemoTotalsTest * MAGETWO-97825: Skipped Randomly failing test AdminCreateCreditMemoWhenCartRuleDeletedTest * Fixed-19800: Contact us : design improvement. * Fixed 19800 Contact us : design improvement * Fixed 19800 Contact us : design improvement * Fixed 19800 Contact us : design improvement * Fixed 19800 Contact us : design improvement * Fixed 19800 Contact us : design improvement * ENGCOM-3763: Static test fix. * Fixed-19791: Logo vertical misalignment. * Fixed-Checkbox-Alignement: Checkbox alignment issue. * MAGETWO-72879: Update product from mini shopping cart doesn't reflect in the shopping cart * MAGETWO-96711: One page Checkout resets Customer data if Product Qty was changed * MAGETWO-94348: The cart rule cannot affect the Bundle product with Fixed Price * magento/magento2#18362: Fixed static tests * Added constants to unit codes to make it easier to reuse it if necessary. * ENGCOM-3774: Static test fix. * MAGETWO-96815: There is no option to remove the Store Credit from the payment page of checkout * Fix negative credit memo bug caused by credit memo discount total calculator * ENGCOM-3720: Static test fix. * Added required error message. If any JSON error occurs user will be be shown error message if the app mode is not in Production. * Fixed Unused Local variable * Fixed unused local variable * Removed use of Class ObjectManager in Construct * Updated to show error message to all Env. * Update lib/internal/Magento/Framework/Serialize/Serializer/Json.php Co-Authored-By: vinogcs * ENGCOM-3782: Static test fix. * Add useful debug info for which website has not been found * Add with code/with id text * MAGETWO-73598: CLONE - Quans: Orders API without key/value on ADDITIONAL_INFORMATION (only values) * MAGETWO-96815: There is no option to remove the Store Credit from the payment page of checkout * Issue fixed #19985 Send email confirmation popup close button area overlapping to content Issue fixed #19985 Send email confirmation popup close button area overlapping to content * Issue fixed #19985 Send email confirmation popup close button area overlapping Issue fixed #19985 Send email confirmation popup close button area overlapping * Order API resources updated. * 'wishlist-page-edit-remove-item-misalign' :: On wish list page edit, remove item misalign in 640 X 767 resolution * Order-view-invoices :: Order view invoices template not display proper on ipad * Updated CancelOrderItemObserver.php Fixed Issue #20121 Cancel order increases stock although "Set Items' Status to be In Stock When Order is Cancelled" is set to No * ENGCOM-3845: Static test fix. * MAGETWO-74604: Event clean_cache_by_tags didn't clean all tags pointed in getIdentity * MAGETWO-73598: CLONE - Quans: Orders API without key/value on ADDITIONAL_INFORMATION (only values) * MAGETWO-97899: [FT] [MFTF] AdminAvailabilityCreditMemoWithNoPaymentTest fails because of bad design * MAGETWO-94157: Issue with products with customizable options * MAGETWO-94348: The cart rule cannot affect the Bundle product with Fixed Price * MAGETWO-93832: Automate with MFTF Copy Single Update to Another Update * MAGETWO-73598: CLONE - Quans: Orders API without key/value on ADDITIONAL_INFORMATION (only values) * Static test fix. * MAGETWO-95276: Varnish "Connection reset by peer" error when large catalog is reindexed on schedule * MAGETWO-95176: Detaching category from product causes massive product url regeneration * MAGETWO-94348: The cart rule cannot affect the Bundle product with Fixed Price * MAGETWO-73625: Products change in one category will cause cache miss for other categories (Only reproducible by unassigning products from backend category page) * admin-order-info-issue2.2 * MAGETWO-94348: The cart rule cannot affect the Bundle product with Fixed Price * MAGETWO-94348: The cart rule cannot affect the Bundle product with Fixed Price * Fix static test. * MAGETWO-95176: Detaching category from product causes massive product url regeneration * #20376 Fix issue with file uploading if an upload field is disabled * #20461 Add translate component * (Fixes issue #18027) Round the total amount to 4 decimal places, to avoid floating point overflows * round the total amount only if it is float * round base total amount * cast formatted number to float * thousand separator removed from number format * use round instead of number_format to round the total amount * unnecessary comments removed * ENGCOM-3464: Static tests fixed. * fixed Negative order amount in dashboard - #18754 * Fix static test. * Update styles-old.less * update-button-issue-while-updating-billing-and-shipping-address * MAGETWO-95752: Unable to create Configurations for Configurable Products * issue fixed #20563 Go to shipping information, Update qty & Addresses and Enter a new address button Not aligned from left and right in 767px screen size issue fixed #20563 Go to shipping information, Update qty & Addresses and Enter a new address button Not aligned from left and right in 767px screen size * recent-order-product-title-misaligned * Fix static tests. * Fix static tests. * MAGETWO-94348: The cart rule cannot affect the Bundle product with Fixed Price * MAGETWO-95752: Unable to create Configurations for Configurable Products * MAGETWO-95752: Unable to create Configurations for Configurable Products * MAGETWO-95752: Unable to create Configurations for Configurable Products * MAGETWO-94114: Cannot refresh Captcha image in production mode * MQE-1420: Bump MFTF version in Magento - MFTF version bump + allure-codeception dependency update. * MQE-1420: Bump MFTF version in Magento - changing argument from its own attribute to part of the command * Fixed issue of Lifetime update syntax error * Fix static test. --- .../Order/View/Info/PaymentDetails.php | 28 ++ .../Magento/Authorizenet/Model/Directpost.php | 29 +- app/code/Magento/Authorizenet/etc/config.xml | 1 + app/code/Magento/Authorizenet/etc/di.xml | 5 + app/code/Magento/Authorizenet/i18n/en_US.csv | 6 + .../order/view/info/fraud_details.phtml | 4 +- .../Block/Widget/Form/Element/Dependence.php | 2 +- .../Grid/Massaction/AbstractMassaction.php | 17 +- .../Magento/Backend/Model/Search/Customer.php | 2 +- .../Mftf/Section/AdminMainActionsSection.xml | 1 + .../Mftf/Section/AdminMessagesSection.xml | 4 +- .../Section/AdminSlideOutDialogSection.xml | 17 + .../templates/system/cache/additional.phtml | 6 +- .../templates/widget/tabshoriz.phtml | 2 +- .../Controller/Paypal/PlaceOrder.php | 6 +- .../Model/Paypal/Helper/OrderPlace.php | 30 +- .../Model/Paypal/OrderCancellationService.php | 76 ++++ .../Braintree/Plugin/OrderCancellation.php | 79 ++++ ...rderFillBraintreeCreditCardActionGroup.xml | 4 - ...inCreateOrderUsingBraintreePaymentTest.xml | 5 +- .../Unit/Controller/Paypal/PlaceOrderTest.php | 142 +++--- .../Model/Paypal/Helper/OrderPlaceTest.php | 135 +++--- app/code/Magento/Braintree/etc/di.xml | 4 + app/code/Magento/Braintree/i18n/en_US.csv | 1 + .../view/adminhtml/templates/form/cc.phtml | 2 +- .../Fieldset/Options/Type/Checkbox.php | 15 +- .../Composite/Fieldset/Options/Type/Multi.php | 15 +- .../Bundle/Pricing/Adjustment/Calculator.php | 2 +- .../BundleProductsOnAdminActionGroup.xml | 10 + .../Test/Mftf/Page/StorefrontProductPage.xml | 2 +- .../Section/AdminProductFormBundleSection.xml | 3 + .../StorefrontProductInfoMainSection.xml | 3 +- .../sales/order/items/renderer.phtml | 141 +++--- .../Model/Import/Product/Type/BundleTest.php | 8 +- .../CacheInvalidate/Model/PurgeCache.php | 76 +++- .../Block/Product/ProductList/Toolbar.php | 82 +++- .../Adminhtml/Product/Attribute/Validate.php | 2 +- .../Catalog/Controller/Category/View.php | 19 +- .../Category/Product/PositionResolver.php | 2 + .../Indexer/Product/Flat/Action/Indexer.php | 35 +- .../Model/Indexer/Product/Flat/Action/Row.php | 12 +- .../Indexer/Product/Flat/FlatTableBuilder.php | 22 +- .../Indexer/Product/Flat/TableBuilder.php | 11 - app/code/Magento/Catalog/Model/Product.php | 2 +- .../Model/Product/Gallery/CreateHandler.php | 2 +- .../Product/ProductList/ToolbarMemorizer.php | 183 ++++++++ .../Catalog/Model/ResourceModel/Category.php | 14 +- .../Model/ResourceModel/Category/Flat.php | 14 +- .../RedundantCategoryImageChecker.php | 52 +++ .../Model/ResourceModel/Product/Gallery.php | 21 +- .../Model/ResourceModel/Product/Image.php | 13 +- .../Framework/App/Action/ContextPlugin.php | 74 ++++ .../Category/RemoveRedundantImagePlugin.php | 77 ++++ .../AddProductToCartActionGroup.xml | 14 +- .../ActionGroup/AdminCategoryActionGroup.xml | 35 ++ .../ActionGroup/AdminProductActionGroup.xml | 46 +- .../AdminProductAttributeActionGroup.xml | 7 +- .../AdminProductAttributeSetActionGroup.xml | 1 + .../AdminProductGridActionGroup.xml | 22 +- .../SearchForProductOnBackendActionGroup.xml | 9 +- ...artCustomOptionsProductPageActionGroup.xml | 1 - .../StorefrontCategoryActionGroup.xml | 30 ++ .../Mftf/Data/CatalogStorefrontConfigData.xml | 49 +++ .../Catalog/Test/Mftf/Data/CategoryData.xml | 8 + .../Mftf/Data/ProductAttributeOptionData.xml | 18 +- .../Data/ProductConfigurableAttributeData.xml | 3 +- .../Catalog/Test/Mftf/Data/ProductData.xml | 14 + .../Catalog/Test/Mftf/Data/StoreLabelData.xml | 18 +- .../Metadata/catalog_configuration-meta.xml | 118 +++++ .../Test/Mftf/Page/AdminCategoryEditPage.xml | 2 + .../Test/Mftf/Page/AdminCategoryPage.xml | 4 +- .../Test/Mftf/Page/AdminProductIndexPage.xml | 1 + .../Test/Mftf/Page/StorefrontCategoryPage.xml | 1 + .../AdminCategoryBasicFieldSection.xml | 5 +- .../Section/AdminCategoryContentSection.xml | 15 + .../AdminCategoryDisplaySettingsSection.xml | 15 + ...AdminProductFormAdvancedPricingSection.xml | 1 + .../Mftf/Section/AdminProductFormSection.xml | 6 +- .../Mftf/Section/AdminProductGridSection.xml | 3 +- .../AdminProductGridTableHeaderSection.xml | 14 + .../StorefrontCategoryPagerSection.xml | 17 + .../Section/StorefrontMessagesSection.xml | 3 +- .../StorefrontProductInfoMainSection.xml | 3 +- .../Section/StorefrontProductPageSection.xml | 6 +- ...CategoryProductsUsingScopeSelectorTest.xml | 43 +- ...edFieldsHaveRequiredFieldIndicatorTest.xml | 53 +++ ...ingTierPriceWithEmptyDiscountValueTest.xml | 55 +++ .../Test/CheckTierPricingOfProductsTest.xml | 15 +- ...ntPurchaseProductWithCustomOptionsTest.xml | 52 ++- ...orefrontRememberCategoryPaginationTest.xml | 70 +++ .../Block/Product/ProductList/ToolbarTest.php | 28 +- .../Category/Product/PositionResolverTest.php | 2 +- .../Indexer/Product/Flat/Action/RowTest.php | 8 +- .../ProductList/ToolbarMemorizerTest.php | 213 +++++++++ .../Catalog/Test/Unit/Model/ProductTest.php | 12 +- .../Model/ResourceModel/Product/ImageTest.php | 237 ++++++++++ .../App/Action/ContextPluginTest.php | 78 ++++ .../Product/Listing/Collector/ImageTest.php | 46 +- .../Product/Form/Modifier/TierPrice.php | 11 +- .../Product/Listing/Collector/Image.php | 17 +- .../Magento/Catalog/etc/adminhtml/system.xml | 5 + app/code/Magento/Catalog/etc/config.xml | 1 + app/code/Magento/Catalog/etc/di.xml | 8 + app/code/Magento/Catalog/etc/frontend/di.xml | 3 + app/code/Magento/Catalog/i18n/en_US.csv | 1 + .../catalog/product/attribute/set/main.phtml | 2 +- .../adminhtml/web/catalog/category/edit.js | 2 +- .../Catalog/view/base/web/js/price-box.js | 6 +- .../frontend/web/js/product/list/toolbar.js | 42 +- .../Model/Export/Product.php | 4 + .../Model/Import/Product.php | 15 +- .../Model/Import/Uploader.php | 4 +- .../Block/Stockqty/AbstractStockqty.php | 3 +- .../Model/Stock/StockItemRepository.php | 30 +- .../CatalogInventory/Model/StockRegistry.php | 10 + .../Observer/CancelOrderItemObserver.php | 14 +- .../Mftf/Data/ProductStockOptionsData.xml | 25 ++ .../Metadata/product_stock_options-meta.xml | 21 + .../Model/Stock/StockItemRepositoryTest.php | 2 +- .../Test/Unit/Model/StockRegistryTest.php | 163 ++++++- .../Model/Indexer/AbstractIndexer.php | 7 +- .../CatalogPriceRuleActionGroup.xml | 26 ++ ...hipArePersistedUnderLongTermCookieTest.xml | 82 ++++ .../Indexer/Fulltext/Action/DataProvider.php | 28 +- .../Model/Layer/Filter/Attribute.php | 9 +- .../Model/ResourceModel/Engine.php | 8 +- .../Unit/Model/Layer/Filter/AttributeTest.php | 7 - .../Magento/CatalogSearch/etc/indexer.xml | 2 - .../Category/ChildrenUrlRewriteGenerator.php | 39 +- .../Model/CategoryUrlPathGenerator.php | 17 +- ...ategoryProcessUrlRewriteSavingObserver.php | 53 ++- .../CategoryUrlPathAutogeneratorObserver.php | 81 +++- .../Observer/UrlRewriteHandler.php | 36 +- ...iteStoreLevelUrlKeyOfChildCategoryTest.xml | 66 +++ .../ChildrenUrlRewriteGeneratorTest.php | 22 +- .../Model/CategoryUrlPathGeneratorTest.php | 55 +++ ...oryProcessUrlRewriteSavingObserverTest.php | 129 ++++++ ...tegoryUrlPathAutogeneratorObserverTest.php | 34 +- .../AdminCreateBlockWithWidgetActionGroup.xml | 45 ++ ...nCatalogProductListWidgetOperatorsTest.xml | 145 ++++++ .../Api/Data/PaymentDetailsInterface.php | 4 +- .../Block/Checkout/AttributeMerger.php | 13 +- app/code/Magento/Checkout/Block/Onepage.php | 22 +- .../Magento/Checkout/CustomerData/Cart.php | 9 +- .../Checkout/Model/DefaultConfigProvider.php | 84 +++- .../Model/ShippingInformationManagement.php | 10 +- .../Observer/SalesQuoteSaveAfterObserver.php | 10 +- .../Mftf/ActionGroup/CheckoutActionGroup.xml | 32 ++ .../GoToCheckoutFromMinicartActionGroup.xml | 5 +- ...ckoutFillingShippingSectionActionGroup.xml | 9 + ...NewAddressInShippingSectionActionGroup.xml | 12 +- .../StorefrontMiniCartActionGroup.xml | 38 ++ .../Section/CheckoutCartProductSection.xml | 3 + .../Mftf/Section/CheckoutPaymentSection.xml | 5 + .../Mftf/Section/CheckoutShippingSection.xml | 4 +- .../StorefrontCheckoutCartSummarySection.xml | 1 + .../Section/StorefrontMinicartSection.xml | 3 + .../CheckNotVisibleProductInMinicartTest.xml | 70 +++ ...frontCheckoutTotalsSortOrderInCartTest.xml | 46 ++ .../Test/StorefrontCustomerCheckoutTest.xml | 1 + ...tWithRestrictedCountriesForPaymentTest.xml | 59 +++ ...StorefrontGuestCheckoutDataPersistTest.xml | 57 +++ .../Mftf/Test/StorefrontGuestCheckoutTest.xml | 35 +- ...tWithRestrictedCountriesForPaymentTest.xml | 70 +++ ...ntOnePageCheckoutDataWhenChangeQtyTest.xml | 92 ++++ ...aultShippingAddressForVirtualQuoteTest.xml | 54 +++ ...nShoppingCartAfterUpdateInMinicartTest.xml | 61 +++ .../Block/Checkout/AttributeMergerTest.php | 122 ++++++ .../Checkout/Test/Unit/Block/OnepageTest.php | 9 +- .../SalesQuoteSaveAfterObserverTest.php | 5 +- app/code/Magento/Checkout/etc/frontend/di.xml | 1 + .../Checkout/etc/frontend/sections.xml | 1 - app/code/Magento/Checkout/i18n/en_US.csv | 1 + .../view/adminhtml/email/failed_payment.html | 20 +- .../frontend/layout/checkout_index_index.xml | 4 + .../frontend/templates/cart/minicart.phtml | 8 + .../view/frontend/web/js/checkout-data.js | 36 +- .../web/js/model/checkout-data-resolver.js | 23 +- .../view/frontend/web/js/model/place-order.js | 22 +- .../web/js/model/postcode-validator.js | 8 +- .../web/js/model/shipping-rates-validator.js | 19 +- .../Checkout/view/frontend/web/js/sidebar.js | 5 + .../web/js/view/form/element/email.js | 11 +- .../view/frontend/web/js/view/shipping.js | 1 + .../js/view/summary/item/details/message.js | 30 ++ .../web/template/summary/item/details.html | 3 + .../summary/item/details/message.html | 9 + .../NavigateToCMSPagesActionGroup.xml | 22 + .../Cms/Test/Mftf/Data/CmsBlockData.xml | 17 + .../Cms/Test/Mftf/Metadata/cms_block-meta.xml | 23 + .../Test/Mftf/Page/AdminCmsBlockEditPage.xml | 14 + .../Test/Mftf/Page/AdminCmsBlockGridPage.xml | 13 + .../Section/AdminCmsBlockContentSection.xml | 15 + .../CmsNewPagePageBasicFieldsSection.xml | 3 +- .../Section/CmsPagesPageActionsSection.xml | 2 +- .../Config/Block/System/Config/Form.php | 44 +- .../Command/ConfigSet/DefaultProcessor.php | 32 +- app/code/Magento/Config/Model/Config.php | 117 ++++- .../Backend/Currency/AbstractCurrency.php | 13 +- .../Mftf/Data/WebUrlOptionsConfigData.xml | 24 + .../Metadata/web_url_options_config-meta.xml | 22 + .../Unit/Block/System/Config/FormTest.php | 6 +- .../ConfigSet/DefaultProcessorTest.php | 64 +-- .../Config/Test/Unit/Model/ConfigTest.php | 332 ++++++++------ app/code/Magento/Config/etc/module.xml | 2 +- .../Model/Export/RowCustomizer.php | 27 +- .../ConfigurableImportExport/composer.json | 3 +- .../Block/Cart/Item/Renderer/Configurable.php | 8 + .../Product/Indexer/Price/Configurable.php | 46 +- .../Pricing/Renderer/SalableResolver.php | 21 +- ...inCreateConfigurableProductActionGroup.xml | 25 ++ .../AdminProductFormConfigurationsSection.xml | 4 +- ...nCheckValidatorConfigurableProductTest.xml | 116 +++++ ...gurableProductWithFileCustomOptionTest.xml | 1 - ...ConfigurableWithCatalogRuleAppliedTest.xml | 3 - .../Cart/Item/Renderer/ConfigurableTest.php | 56 +++ .../Pricing/Renderer/SalableResolverTest.php | 87 ++++ .../web/js/components/price-configurable.js | 2 + .../adminhtml/web/js/variations/variations.js | 6 +- .../templates/product/price/tier_price.phtml | 11 +- .../view/frontend/web/js/configurable.js | 77 ++-- .../view/frontend/email/submitted_form.html | 8 +- .../view/frontend/web/css/source/_module.less | 42 ++ .../system/currency/rate/matrix.phtml | 4 +- .../PostCodesPatternsAttributeData.php | 50 +++ .../Customer/Model/AccountManagement.php | 106 ++++- .../Customer/Model/EmailNotification.php | 25 +- .../Model/Metadata/CustomerMetadata.php | 19 +- .../Model/Metadata/Form/AbstractData.php | 13 +- .../ResourceModel/CustomerRepository.php | 2 +- .../Mftf/ActionGroup/CustomerActionGroup.xml | 2 - .../OpenEditCustomerFromAdminActionGroup.xml | 7 +- .../StorefrontCustomerAccountActionGroup.xml | 20 + .../StorefrontCustomerActionGroup.xml | 18 + .../Customer/Test/Mftf/Data/AddressData.xml | 12 +- .../Test/Mftf/Data/CustomerConfigData.xml | 24 + .../Customer/Test/Mftf/Data/CustomerData.xml | 14 +- .../customer_config_account_sharing-meta.xml | 21 + .../Mftf/Page/StorefrontCustomerOrderPage.xml | 14 - .../Page/StorefrontCustomerOrderViewPage.xml | 5 +- .../Page/StorefrontCustomerOrdersGridPage.xml | 14 + .../StorefrontCustomerOrdersGridSection.xml | 13 + .../Section/StorefrontPanelHeaderSection.xml | 7 +- .../Test/Unit/Model/EmailNotificationTest.php | 83 +++- .../Model/Metadata/Form/AbstractDataTest.php | 43 +- .../ResourceModel/CustomerRepositoryTest.php | 2 +- .../Listing/Column/ValidationRulesTest.php | 44 +- .../Listing/Column/ValidationRules.php | 4 + app/code/Magento/Customer/etc/di.xml | 7 + .../email/account_new_confirmation.html | 6 +- .../frontend/layout/customer_address_form.xml | 1 + .../frontend/templates/address/edit.phtml | 7 +- .../view/frontend/web/js/addressValidation.js | 86 +++- .../view/frontend/web/js/customer-data.js | 3 + .../Magento/Deploy/Collector/Collector.php | 53 ++- .../Model/Config/Source/WeightUnit.php | 18 +- .../Eav/Model/Attribute/Data/AbstractData.php | 9 +- .../Attribute/Frontend/AbstractFrontend.php | 5 + .../Entity/Collection/AbstractCollection.php | 2 +- .../Unit/Model/Attribute/Data/TextTest.php | 136 +++++- .../Frontend/DefaultFrontendTest.php | 167 +++---- .../GiftMessage/Block/Message/Inline.php | 34 +- .../Test/Mftf/Data/GiftOptionsData.xml | 27 ++ .../Test/Mftf/Metadata/gift_options-meta.xml | 24 + .../StorefrontCheckoutGiftOptionsSection.xml | 15 + .../view/frontend/templates/inline.phtml | 3 +- .../AdminGroupedProductActionGroup.xml | 25 ++ .../Test/Mftf/Data/GroupedProductData.xml | 9 + .../Test/Mftf/Page/AdminOrderCreatePage.xml | 14 + .../Test/Mftf/Page/AdminProductCreatePage.xml | 15 + .../AdminAddProductsToGroupPanelSection.xml | 19 + ...minOrderConfigureGroupedProductSection.xml | 15 + ...AdminProductFormGroupedProductsSection.xml | 20 + .../AdminSortingAssociatedProductsTest.xml | 209 +++++++++ .../Product/Form/Modifier/GroupedTest.php | 2 + .../Product/Form/Modifier/Grouped.php | 34 +- .../adminhtml/web/css/grouped-product.css | 46 ++ .../adminhtml/web/js/grouped-product-grid.js | 219 ++++++++++ .../web/template/components/position.html | 15 + .../Controller/Adminhtml/Import/Download.php | 38 +- .../Controller/Adminhtml/Import/Start.php | 9 + .../Magento/ImportExport/Model/Import.php | 32 +- .../Model/Import/SampleFileProvider.php | 127 ++++++ .../Test/Mftf/Page/AdminImportIndexPage.xml | 15 + .../Mftf/Section/AdminImportHeaderSection.xml | 14 + .../Mftf/Section/AdminImportMainSection.xml | 17 + ...inImportProductsWithDeleteBehaviorTest.xml | 65 +++ .../Model/Import/SampleFileProviderTest.php | 155 +++++++ app/code/Magento/ImportExport/etc/di.xml | 12 + ...StorefrontCheckingResultsOfFiltersTest.xml | 11 +- ...rontMultishippingCheckoutAddressesPage.xml | 15 + ...frontMultishippingCheckoutShippingPage.xml | 15 + .../StorefrontCheckoutCartSummarySection.xml | 14 + ...StorefrontMultishippingCheckoutSection.xml | 15 + ...ultishipping_checkout_customer_address.xml | 1 + .../Controller/Subscriber/NewAction.php | 5 +- .../Model/ResourceModel/Subscriber.php | 57 +-- .../Magento/Newsletter/Model/Subscriber.php | 12 +- .../Test/Unit/Model/SubscriberTest.php | 16 +- .../Newsletter/etc/adminhtml/system.xml | 18 +- .../templates/preview/iframeswitcher.phtml | 9 +- .../Data/PaymentAdditionalInfoInterface.php | 16 + .../Payment/Model/PaymentAdditionalInfo.php | 60 +++ app/code/Magento/Payment/etc/di.xml | 1 + .../templates/transparent/iframe.phtml | 2 +- .../Controller/Express/AbstractExpress.php | 18 +- .../Config/Structure/Element/FieldPlugin.php | 16 +- app/code/Magento/Paypal/Model/Express.php | 2 +- .../Structure/Element/FieldPluginTest.php | 24 +- .../Persistent/Block/Header/Additional.php | 53 ++- .../Persistent/CustomerData/Persistent.php | 71 +++ ...uestPaymentInformationManagementPlugin.php | 4 +- .../Magento/Persistent/Model/Observer.php | 7 +- .../Plugin/PersistentCustomerContext.php | 39 ++ .../Magento/Persistent/Model/QuoteManager.php | 3 +- .../StorefrontCustomerActionGroup.xml | 22 + .../Test/Mftf/Data/PersistentData.xml | 14 + .../Mftf/Metadata/perstistent_config-meta.xml | 3 + .../StorefrontCustomerSignInFormSection.xml | 14 + ...omeMessageAfterCustomerIsLoggedOutTest.xml | 79 ++++ .../Test/Unit/Block/Header/AdditionalTest.php | 246 ++--------- ...PaymentInformationManagementPluginTest.php | 3 +- .../Test/Unit/Model/ObserverTest.php | 23 +- .../Test/Unit/Model/QuoteManagerTest.php | 11 +- app/code/Magento/Persistent/etc/di.xml | 3 + .../Magento/Persistent/etc/frontend/di.xml | 14 + .../Persistent/etc/frontend/sections.xml | 13 + .../view/frontend/requirejs-config.js | 14 + .../view/frontend/templates/additional.phtml | 21 + .../web/js/view/additional-welcome.js | 55 +++ .../web/js/view/customer-data-mixin.js | 51 +++ .../Catalog/Product/Gallery/CreateHandler.php | 54 +++ .../AdminProductVideoActionGroup.xml | 46 ++ .../StorefrontProductVideoActionGroup.xml | 17 + .../Test/Mftf/Data/ProductVideoConfigData.xml | 26 ++ .../Test/Mftf/Data/ProductVideoData.xml | 16 + .../Metadata/product_video_config-meta.xml | 21 + .../Test/Mftf/Page/AdminProductCreatePage.xml | 14 + .../Section/AdminProductImagesSection.xml | 16 + .../Section/AdminProductNewVideoSection.xml | 17 + .../StorefrontProductInfoMainSection.xml | 18 + ...ontYoutubeVideoWindowOnProductPageTest.xml | 95 ++++ .../Magento/Quote/Api/Data/CartInterface.php | 4 +- .../Quote/Model/Quote/Address/Total.php | 7 + .../Model/Quote/Address/Total/Shipping.php | 7 +- .../Quote/Model/Quote/Item/Updater.php | 4 +- .../Unit/Model/Quote/Item/UpdaterTest.php | 10 +- .../Block/Adminhtml/Sales/Tax/Grid.php | 1 - .../Model/ResourceModel/Order/Collection.php | 5 +- .../Mftf/Page/AdminReportSalesTaxPage.xml | 15 + .../Section/AdminReportMainActionSection.xml | 14 + .../Mftf/Section/AdminReportTaxSection.xml | 19 + .../Test/Mftf/Test/AdminTaxReportGridTest.xml | 130 ++++++ .../Review/Model/ResourceModel/Rating.php | 4 +- .../Rule/Model/Condition/Sql/Builder.php | 4 + .../Unit/Model/Condition/Sql/BuilderTest.php | 65 +++ .../Adminhtml/Order/Create/Form/Account.php | 50 ++- .../Adminhtml/Order/Create/Sidebar/Cart.php | 35 ++ .../Order/Item/Renderer/DefaultRenderer.php | 17 + .../Controller/Adminhtml/Order/AddComment.php | 24 +- .../Model/CronJob/CleanExpiredOrders.php | 28 +- app/code/Magento/Sales/Model/Order.php | 53 ++- .../Model/Order/Creditmemo/Total/Tax.php | 6 +- .../Sales/Model/Order/CreditmemoFactory.php | 3 + .../Sales/Model/Order/CustomerAssignment.php | 59 +++ .../Model/Order/Webapi/ChangeOutputArray.php | 59 +++ .../Magento/Sales/Model/OrderRepository.php | 92 +++- .../ResourceModel/Order/Handler/State.php | 34 +- .../AssignOrderToCustomerObserver.php | 23 +- .../ActionGroup/AdminInvoiceActionGroup.xml | 55 +++ .../ActionGroup/AdminOrderActionGroup.xml | 38 +- .../ActionGroup/AdminOrderGridActionGroup.xml | 2 - .../StorefrontSearchGuestOrderActionGroup.xml | 26 ++ .../Sales/Test/Mftf/Data/ConstData.xml | 20 + .../Sales/Test/Mftf/Data/SalesConfigData.xml | 19 + .../Test/Mftf/Metadata/sales_config-meta.xml | 44 ++ .../Test/Mftf/Page/AdminInvoiceNewPage.xml | 2 + .../Mftf/Page/AdminOrderAddressEditPage.xml | 15 + .../Test/Mftf/Page/AdminOrderCreatePage.xml | 1 + .../Test/Mftf/Page/AdminOrderDetailsPage.xml | 4 +- .../Mftf/Page/AdminOrderInvoiceViewPage.xml | 14 + .../Page/StorefrontGuestOrderSearchPage.xml | 14 + .../Page/StorefrontGuestOrderViewPage.xml | 14 + .../Section/AdminCreditMemoItemsSection.xml | 2 +- .../Section/AdminCreditMemoTotalSection.xml | 7 +- .../AdminInvoiceAddressInformationSection.xml | 17 + .../Mftf/Section/AdminInvoiceItemsSection.xml | 4 +- .../AdminInvoiceMainActionsSection.xml | 4 +- ...voiceOrderAndAccountInformationSection.xml | 16 + .../Section/AdminOrderAddressEditSection.xml | 13 + .../AdminOrderAddressInformationSection.xml | 1 + .../AdminOrderFormBundleProductSection.xml | 15 + .../Section/AdminOrderFormItemsSection.xml | 3 + ...dminOrderInvoiceViewMainActionsSection.xml | 14 + .../AdminOrderStoreScopeTreeSection.xml | 16 + .../StorefrontGuestOrderSearchSection.xml | 18 + .../StorefrontGuestOrderViewSection.xml | 14 + ...inAbleToShipPartiallyInvoicedItemsTest.xml | 26 +- ...vailabilityCreditMemoWithNoPaymentTest.xml | 13 +- .../AdminCheckingCreditMemoTotalsTest.xml | 108 +++++ ...reateCreditMemoWhenCartRuleDeletedTest.xml | 91 ++++ .../AdminCreateOrderWithBundleProductTest.xml | 111 +++++ ...reateOrderWithMinimumAmountEnabledTest.xml | 3 +- ...ditMemoTotalAfterShippingDiscountTest.xml} | 18 +- ...minSubmitsOrderWithAndWithoutEmailTest.xml | 5 +- .../Item/Renderer/DefaultRendererTest.php | 29 +- .../Adminhtml/Order/AddCommentTest.php | 186 ++++++++ .../Model/CronJob/CleanExpiredOrdersTest.php | 22 +- .../Model/Order/Creditmemo/Total/TaxTest.php | 8 +- .../Order/Webapi/ChangeOutputArrayTest.php | 92 ++++ .../Test/Unit/Model/OrderRepositoryTest.php | 60 ++- .../ResourceModel/Order/Handler/StateTest.php | 201 ++++----- .../AssignOrderToCustomerObserverTest.php | 20 +- .../ViewModel/Customer/AddressFormatter.php | 131 ++++++ app/code/Magento/Sales/etc/di.xml | 8 + .../Sales/etc/extension_attributes.xml | 3 + app/code/Magento/Sales/etc/webapi.xml | 90 ++-- app/code/Magento/Sales/etc/webapi_rest/di.xml | 7 + app/code/Magento/Sales/etc/webapi_soap/di.xml | 7 + app/code/Magento/Sales/i18n/en_US.csv | 2 + .../sales_order_create_customer_block.xml | 14 +- .../layout/sales_order_create_index.xml | 14 +- ...rder_create_load_block_billing_address.xml | 7 +- .../sales_order_create_load_block_data.xml | 14 +- ...der_create_load_block_shipping_address.xml | 7 +- .../templates/items/column/name.phtml | 2 +- .../templates/order/create/form/address.phtml | 25 +- .../order/invoice/create/items.phtml | 4 +- .../adminhtml/templates/order/view/info.phtml | 2 +- .../adminhtml/web/order/create/scripts.js | 131 ++++-- .../templates/email/shipment/track.phtml | 29 +- .../view/frontend/templates/guest/form.phtml | 2 +- .../Model/ResourceModel/Coupon/Usage.php | 4 +- .../Rule/Condition/Product/Subselect.php | 8 +- ...onDataAfterOrderCustomerAssignObserver.php | 52 +++ .../AdminCartPriceRuleActionGroup.xml | 4 +- .../AdminCreateCartPriceRuleActionGroup.xml | 27 ++ .../AdminEditCartPriceRuleActionGroup.xml | 18 + .../AdminFilterCartPriceRuleActionGroup.xml | 20 + .../ApplyCartRuleOnStorefrontActionGroup.xml | 22 + .../StorefrontSalesRuleActionGroup.xml | 18 + .../SalesRule/Test/Mftf/Data/CouponData.xml | 7 + .../Test/Mftf/Data/SalesRuleData.xml | 95 +++- .../Data/SalesRuleProductConditionsData.xml | 16 + .../Test/Mftf/Metadata/coupon-meta.xml | 2 +- .../Mftf/Page/AdminCartPriceRuleEditPage.xml | 13 + .../Page/AdminCartPriceRulesLimitPage.xml | 10 + .../AdminCartPriceRuleDiscountSection.xml | 8 +- .../AdminCartPriceRulesFormSection.xml | 13 + .../Section/AdminCartPriceRulesSection.xml | 6 +- ...inCartRulesAppliedForProductInCartTest.xml | 107 +++++ app/code/Magento/SalesRule/etc/events.xml | 3 + .../frontend/layout/checkout_cart_index.xml | 12 +- .../Block/Adminhtml/Order/Packaging.php | 3 +- .../Model/Carrier/AbstractCarrierOnline.php | 8 + .../ActionGroup/AdminShipmentActionGroup.xml | 50 +++ .../Test/Mftf/Page/AdminShipmentNewPage.xml | 2 + ...AdminShipmentAddressInformationSection.xml | 17 + .../Section/AdminShipmentItemsSection.xml | 3 +- .../AdminShipmentMainActionsSection.xml | 4 +- ...pmentOrderAndAccountInformationSection.xml | 16 + .../Magento/Store/Model/WebsiteRepository.php | 18 +- .../AdminCreateWebsiteActionGroup.xml | 14 + .../AdminSwitchStoreViewActionGroup.xml | 22 + .../StorefrontSwitchStoreViewActionGroup.xml | 19 + .../Test/Mftf/Data/ProductWebsiteLinkData.xml | 14 + .../Store/Test/Mftf/Data/StoreData.xml | 20 +- .../Store/Test/Mftf/Data/WebsiteData.xml | 3 + .../Metadata/product_website_link-meta.xml | 18 + .../Mftf/Page/StorefrontStoreHomePage.xml | 12 + .../AdminNewStoreViewActionsSection.xml | 2 +- .../Url/Plugin/RouteParamsResolverTest.php | 6 +- .../Store/Url/Plugin/RouteParamsResolver.php | 2 +- app/code/Magento/Store/etc/di.xml | 1 + app/code/Magento/Store/etc/frontend/di.xml | 2 +- .../Swatches/Controller/Ajax/Media.php | 1 + app/code/Magento/Swatches/Helper/Data.php | 41 +- ...tSwatchProductWithFileCustomOptionTest.xml | 1 - .../view/frontend/web/js/swatch-renderer.js | 25 +- .../ResourceModel/Report/Tax/Createdat.php | 5 +- .../Mftf/ActionGroup/AdminTaxActionGroup.xml | 45 ++ .../Tax/Test/Mftf/Data/TaxClassData.xml | 18 + .../Tax/Test/Mftf/Data/TaxConfigData.xml | 13 + .../Tax/Test/Mftf/Data/TaxRateData.xml | 27 ++ .../Tax/Test/Mftf/Metadata/tax_class-meta.xml | 24 + .../Test/Mftf/Metadata/tax_config-meta.xml | 9 +- .../Tax/Test/Mftf/Metadata/tax_rate-meta.xml | 25 ++ .../Test/Mftf/Page/AdminTaxRuleNewPage.xml | 15 + .../Test/Mftf/Page/AdminTaxRulesGridPage.xml | 15 + .../Section/AdminTaxRuleEditFormSection.xml | 17 + .../Mftf/Section/AdminTaxRulesGridSection.xml | 14 + ...oppingCartForCustomerPhysicalQuoteTest.xml | 13 +- ...hoppingCartForCustomerVirtualQuoteTest.xml | 13 +- .../Tax/view/base/web/js/price/adjustment.js | 2 +- .../Magento/Theme/Block/Html/Header/Logo.php | 4 + app/code/Magento/Theme/Block/Html/Topmenu.php | 14 +- .../Mftf/Section/StorefrontHeaderSection.xml | 13 + .../Test/Unit/Block/Html/TopmenuTest.php | 3 +- .../view/frontend/templates/html/header.phtml | 4 +- .../frontend/templates/html/header/logo.phtml | 24 +- .../Ui/Component/Listing/Columns/Date.php | 24 + .../Ui/TemplateEngine/Xhtml/Result.php | 14 +- .../AdminDataGridFilterActionGroup.xml | 2 +- .../Section/AdminDataGridTableSection.xml | 5 +- .../Mftf/Section/AdminMessagesSection.xml | 1 + .../Section/StorefrontMessagesSection.xml | 14 + .../Component/Listing/Columns/DateTest.php | 89 +++- .../Unit/TemplateEngine/Xhtml/ResultTest.php | 14 + app/code/Magento/Ui/i18n/en_US.csv | 1 + .../base/web/js/form/element/file-uploader.js | 9 +- .../view/base/web/js/form/element/wysiwyg.js | 21 +- .../web/js/lib/knockout/bindings/bootstrap.js | 2 +- .../js/lib/knockout/extender/bound-nodes.js | 4 +- .../view/base/web/js/lib/validation/rules.js | 18 + app/code/Magento/Ups/Model/Carrier.php | 8 + .../Section/AdminUrlRewriteIndexSection.xml | 3 +- ...tipleStoreviewsDuringProductImportTest.xml | 114 +++++ .../Magento/User/Test/Mftf/Data/UserData.xml | 16 +- .../User/Test/Mftf/Data/UserRoleData.xml | 14 +- .../User/Test/Mftf/Metadata/user-meta.xml | 5 +- app/code/Magento/Usps/Model/Carrier.php | 4 +- .../Mftf/Section/AdminNewWidgetSection.xml | 4 + .../Mftf/Section/StorefrontWidgetsSection.xml | 1 + .../Customer/Wishlist/Item/Column/Cart.php | 26 ++ .../StorefrontCustomerWishlistActionGroup.xml | 22 + ...orefrontCustomerWishlistProductSection.xml | 2 + ...AddMultipleStoreProductsToWishlistTest.xml | 1 - ...StorefrontCheckAmountLimitWishlistTest.xml | 60 +++ .../frontend/templates/item/column/cart.phtml | 3 +- .../view/frontend/web/js/add-to-wishlist.js | 57 +-- .../web/css/source/module/_menu.less | 4 +- .../module/main/_collapsible-blocks.less | 16 + .../main/actions-bar/_store-switcher.less | 14 +- .../web/css/source/_module.less | 8 + .../web/css/source/_module.less | 2 +- .../web/css/source/module/_order.less | 8 + .../adminhtml/Magento/backend/etc/view.xml | 2 + .../web/css/source/components/_messages.less | 4 +- .../css/source/components/_modals_extend.less | 6 +- .../Magento/backend/web/css/styles-old.less | 6 +- .../web/css/source/_module.less | 1 + .../web/css/source/module/_listings.less | 1 - .../web/css/source/_module.less | 2 +- .../web/css/source/_module.less | 1 + .../Magento/blank/web/css/source/_forms.less | 2 +- .../web/css/source/_module.less | 1 + .../web/css/source/module/_listings.less | 1 - .../web/css/source/module/_minicart.less | 5 +- .../css/source/module/checkout/_checkout.less | 1 + .../module/checkout/_order-summary.less | 8 + .../web/css/source/_module.less | 2 +- .../web/css/source/_module.less | 10 +- .../Magento_Theme/web/css/source/_module.less | 23 + .../web/css/source/_module.less | 1 + .../Magento/luma/web/css/source/_forms.less | 2 +- .../css/source/components/_modals_extend.less | 3 +- app/etc/di.xml | 7 + composer.json | 2 +- composer.lock | 32 +- .../tests/_data/catalog_products.csv | 4 + .../acceptance/tests/_data/import_updated.csv | 4 + .../tests/_suite/WYSIWYGDisabledSuite.xml | 23 + .../Customer/Api/CustomerMetadataTest.php | 17 +- .../Service/V1/CreditMemoCreateRefundTest.php | 10 +- .../Magento/Sales/Service/V1/OrderGetTest.php | 38 ++ .../Sales/Service/V1/OrderItemGetTest.php | 33 ++ .../Sales/Service/V1/OrderListTest.php | 90 +++- .../Sales/Service/V1/RefundOrderTest.php | 10 +- .../Test/TestCase/NavigateMenuTest.xml | 2 +- .../Test/Repository/CatalogProductSimple.xml | 2 +- .../ProductTypeSwitchingOnCreationTest.xml | 1 - .../ProductTypeSwitchingOnUpdateTest.php | 1 + .../ProductTypeSwitchingOnUpdateTest.xml | 13 +- .../Edit/Section/Downloadable/Samples.php | 5 +- .../CreateDownloadableProductEntityTest.xml | 175 ++++---- .../Test/Fixture/GroupedProduct.xml | 4 +- .../CreateGroupedProductEntityTest.xml | 2 +- .../TestCase/SalesOrderReportEntityTest.xml | 1 + .../TestCase/SalesRefundsReportEntityTest.xml | 1 + .../Block/Adminhtml/Order/AbstractForm.php | 25 +- ...AssertOrderCancelMassActionFailMessage.php | 10 +- .../Constraint/AssertOrderStatusIsCorrect.php | 4 +- .../Sales/Test/TestCase/GridSortingTest.xml | 1 - .../Test/TestCase/MassOrdersUpdateTest.xml | 10 +- ...MoveLastOrderedProductsOnOrderPageTest.xml | 1 - .../Promo/Quote/Edit/PromoQuoteForm.php | 8 + .../AdvancedSearchWithAttributeTest.xml | 2 - .../SalesShippingReportEntityTest.xml | 1 + .../AddProductToWishlistEntityTest.xml | 1 + .../TestCase/AbstractBackendController.php | 2 +- .../Controller/Paypal/PlaceOrderTest.php | 173 ++++++++ .../Braintree/Fixtures/paypal_quote.php | 29 ++ .../Fixtures/paypal_quote_rollback.php | 9 + .../Controller/Adminhtml/CategoryTest.php | 158 ++++++- .../Flat/Action/CustomFlatAttributeTest.php | 221 ++++++++++ .../Indexer/Product/Flat/Action/RowTest.php | 93 ++-- .../Catalog/Model/ProductExternalTest.php | 3 +- .../Model/RemoveRedundantImageTest.php | 82 ++++ .../Catalog/_files/categories_with_image.php | 53 +++ .../_files/categories_with_image_rollback.php | 23 + .../Catalog/_files/category_product.php | 2 +- .../_files/flat_dropdown_attribute.php | 72 +++ .../flat_dropdown_attribute_rollback.php | 21 + ...t_simple_with_custom_attribute_in_flat.php | 79 ++++ ...with_custom_attribute_in_flat_rollback.php | 41 ++ .../_files/products_in_different_stores.php | 122 ++++++ .../products_in_different_stores_rollback.php | 46 ++ .../Model/Import/ProductTest.php | 412 +++++++++++------- .../import_media_non_existing_images.csv | 2 + .../_files/product_import_qty_wrong_sku.csv | 3 + .../product_to_import_with_category.csv | 2 + .../Import/_files/products_error_img.csv | 11 + ...with_backorders_disabled_and_not_0_qty.csv | 2 + .../Indexer/Fulltext/Action/FullTest.php | 23 + .../Model/CategoryUrlRewriteGeneratorTest.php | 90 ++++ .../testsuite/Magento/Cms/Model/BlockTest.php | 38 +- .../testsuite/Magento/Cms/Model/PageTest.php | 59 ++- .../Console/Command/ConfigSetCommandTest.php | 7 +- .../Indexer/Price/ConfigurableTest.php | 102 +++-- .../Customer/Controller/AccountTest.php | 174 +++++--- .../Customer/Model/CustomerMetadataTest.php | 19 +- .../customer_confirmation_config_disable.php | 21 + ...r_confirmation_config_disable_rollback.php | 23 + .../customer_confirmation_config_enable.php | 21 + ...er_confirmation_config_enable_rollback.php | 23 + ...ation_email_address_with_special_chars.php | 37 ++ ...il_address_with_special_chars_rollback.php | 31 ++ .../Email/Model/Template/FilterTest.php | 8 +- .../Magento/Email/Model/TemplateTest.php | 8 +- .../templates/sample_email_content.phtml | 2 +- .../email/cron_error.html | 2 +- .../email/account_new_confirmed.html | 2 +- .../templates/sample_email_content.phtml | 2 +- .../sample_email_content_custom.phtml | 2 +- .../Magento_Customer/email/account_new.html | 2 +- .../email/account_new_confirmation.html | 2 +- .../Newsletter/Controller/SubscriberTest.php | 3 +- .../Newsletter/Model/SubscriberTest.php | 53 ++- .../Magento/Paypal/Controller/ExpressTest.php | 38 ++ .../Magento/Paypal/Model/IpnTest.php | 4 +- .../Block/Header/AdditionalTest.php | 4 +- .../Magento/Persistent/Model/ObserverTest.php | 32 +- .../Quote/Model/Quote/Item/UpdaterTest.php | 60 +++ .../CaseCheckAddingProductReviewTest.php | 120 +++++ .../Magento/Review/_files/disable_config.php | 15 + .../Order/Create/Form/AccountTest.php | 93 +++- .../Adminhtml/Order/Create/SaveTest.php | 157 ++++++- .../AbstractCreditmemoControllerTest.php | 92 ++++ .../Order/Creditmemo/AddCommentTest.php | 102 +++++ .../Adminhtml/Order/Creditmemo/SaveTest.php | 99 +++++ .../Controller/Adminhtml/Order/EmailTest.php | 136 ++++++ .../Invoice/AbstractInvoiceControllerTest.php | 92 ++++ .../Order/Invoice/AddCommentTest.php | 103 +++++ .../Adminhtml/Order/Invoice/EmailTest.php | 88 ++++ .../Adminhtml/Order/Invoice/SaveTest.php | 97 +++++ .../CustomerData/LastOrderedItemsTest.php | 55 +++ .../Magento/Sales/Model/Order/CreateTest.php | 100 +++++ .../_files/guest_quote_with_addresses.php | 68 +++ .../guest_quote_with_addresses_rollback.php | 32 ++ .../testsuite/Magento/Sales/_files/order.php | 3 +- .../Magento/Sales/_files/order_list.php | 42 +- .../Sales/_files/order_list_with_tax.php | 55 +++ .../_files/order_list_with_tax_rollback.php | 8 + ...with_customer_and_multiple_order_items.php | 12 + .../Sales/_files/order_with_discount.php | 73 ++++ .../_files/order_with_discount_rollback.php | 8 + .../_files/order_with_multiple_items.php | 81 ++++ .../order_with_shipping_and_invoice.php | 12 +- .../Magento/Sales/_files/order_with_tax.php | 52 +++ .../Sales/_files/order_with_tax_rollback.php | 8 + .../Sales/_files/quote_with_custom_price.php | 73 ++++ .../quote_with_custom_price_rollback.php | 24 + ...CouponDataAfterOrderCustomerAssignTest.php | 291 +++++++++++++ .../AbstractShipmentControllerTest.php | 92 ++++ .../Order/Shipment/AddCommentTest.php | 102 +++++ .../Adminhtml/Order/Shipment/SaveTest.php | 97 +++++ .../Magento/Store/Model/StoreTest.php | 91 +++- .../Magento/Test/Legacy/ObsoleteCodeTest.php | 4 +- .../Test/Less/_files/blacklist/old.txt | 1 + .../Php/_files/phpcpd/blacklist/common.txt | 2 + .../Magento/Framework/App/ScopeDefault.php | 4 +- .../Framework/Cache/Backend/Database.php | 10 +- .../Framework/DB/Query/BatchRangeIterator.php | 4 +- .../Framework/Data/Form/Element/Date.php | 12 +- .../Framework/Data/Form/Element/Editor.php | 9 + lib/internal/Magento/Framework/Escaper.php | 14 + .../Config/_files/invalidEventsXmlArray.php | 4 - .../Magento/Framework/Event/etc/events.xsd | 2 +- .../Mail/Template/TransportBuilder.php | 19 +- .../Unit/Template/TransportBuilderTest.php | 48 +- .../Reflection/DataObjectProcessor.php | 31 +- .../Adapter/Mysql/Query/Builder/Match.php | 11 +- .../Magento/Framework/Serialize/README.md | 1 + .../Framework/Serialize/Serializer/Json.php | 8 +- .../Serialize/Serializer/JsonHexTag.php | 35 ++ .../Test/Unit/Serializer/JsonHexTagTest.php | 118 +++++ .../Unit/DB/Query/BatchRangeIteratorTest.php | 2 +- .../Framework/Test/Unit/EscaperTest.php | 11 +- .../View/Element/Html/Link/Current.php | 26 +- .../Unit/Element/Html/Link/CurrentTest.php | 49 ++- .../Webapi/Rest/Response/Renderer/Xml.php | 6 +- .../Unit/Rest/Response/Renderer/XmlTest.php | 5 + lib/web/css/docs/forms.html | 2 +- lib/web/css/docs/variables.html | 2 +- lib/web/css/source/lib/_forms.less | 2 + lib/web/css/source/lib/_navigation.less | 24 + lib/web/fotorama/fotorama.js | 17 +- .../wysiwyg/tiny_mce/html5-schema.js | 3 +- lib/web/mage/dataPost.js | 15 +- lib/web/mage/gallery/gallery.js | 10 + lib/web/mage/gallery/gallery.less | 1 + lib/web/mage/menu.js | 6 +- lib/web/mage/validation.js | 7 +- lib/web/magnifier/magnifier.js | 6 +- setup/performance-toolkit/benchmark.jmx | 33 ++ setup/view/magento/setup/data-option.phtml | 4 +- setup/view/magento/setup/extension-grid.phtml | 7 +- setup/view/magento/setup/module-grid.phtml | 2 +- setup/view/magento/setup/select-version.phtml | 2 +- 719 files changed, 18293 insertions(+), 2807 deletions(-) create mode 100644 app/code/Magento/Authorizenet/Block/Adminhtml/Order/View/Info/PaymentDetails.php create mode 100644 app/code/Magento/Backend/Test/Mftf/Section/AdminSlideOutDialogSection.xml create mode 100644 app/code/Magento/Braintree/Model/Paypal/OrderCancellationService.php create mode 100644 app/code/Magento/Braintree/Plugin/OrderCancellation.php create mode 100644 app/code/Magento/Catalog/Model/Product/ProductList/ToolbarMemorizer.php create mode 100644 app/code/Magento/Catalog/Model/ResourceModel/Category/RedundantCategoryImageChecker.php create mode 100644 app/code/Magento/Catalog/Plugin/Framework/App/Action/ContextPlugin.php create mode 100644 app/code/Magento/Catalog/Plugin/Model/ResourceModel/Category/RemoveRedundantImagePlugin.php create mode 100644 app/code/Magento/Catalog/Test/Mftf/Data/CatalogStorefrontConfigData.xml create mode 100644 app/code/Magento/Catalog/Test/Mftf/Metadata/catalog_configuration-meta.xml create mode 100644 app/code/Magento/Catalog/Test/Mftf/Section/AdminCategoryContentSection.xml create mode 100644 app/code/Magento/Catalog/Test/Mftf/Section/AdminCategoryDisplaySettingsSection.xml create mode 100644 app/code/Magento/Catalog/Test/Mftf/Section/AdminProductGridTableHeaderSection.xml create mode 100644 app/code/Magento/Catalog/Test/Mftf/Section/StorefrontCategoryPagerSection.xml create mode 100644 app/code/Magento/Catalog/Test/Mftf/Test/AdminRequiredFieldsHaveRequiredFieldIndicatorTest.xml create mode 100644 app/code/Magento/Catalog/Test/Mftf/Test/AdminValidateApplyingTierPriceWithEmptyDiscountValueTest.xml create mode 100644 app/code/Magento/Catalog/Test/Mftf/Test/StorefrontRememberCategoryPaginationTest.xml create mode 100644 app/code/Magento/Catalog/Test/Unit/Model/Product/ProductList/ToolbarMemorizerTest.php create mode 100644 app/code/Magento/Catalog/Test/Unit/Model/ResourceModel/Product/ImageTest.php create mode 100644 app/code/Magento/Catalog/Test/Unit/Plugin/Framework/App/Action/ContextPluginTest.php create mode 100644 app/code/Magento/CatalogInventory/Test/Mftf/Data/ProductStockOptionsData.xml create mode 100644 app/code/Magento/CatalogInventory/Test/Mftf/Metadata/product_stock_options-meta.xml create mode 100644 app/code/Magento/CatalogRule/Test/Mftf/Test/StorefrontCatalogPriceRuleAndCustomerGroupMembershipArePersistedUnderLongTermCookieTest.xml create mode 100644 app/code/Magento/CatalogUrlRewrite/Test/Mftf/Test/RewriteStoreLevelUrlKeyOfChildCategoryTest.xml create mode 100644 app/code/Magento/CatalogUrlRewrite/Test/Unit/Observer/CategoryProcessUrlRewriteSavingObserverTest.php create mode 100644 app/code/Magento/CatalogWidget/Test/Mftf/ActionGroup/AdminCreateBlockWithWidgetActionGroup.xml create mode 100644 app/code/Magento/CatalogWidget/Test/Mftf/Test/AdminCatalogProductListWidgetOperatorsTest.xml create mode 100644 app/code/Magento/Checkout/Test/Mftf/ActionGroup/StorefrontMiniCartActionGroup.xml create mode 100644 app/code/Magento/Checkout/Test/Mftf/Test/CheckNotVisibleProductInMinicartTest.xml create mode 100644 app/code/Magento/Checkout/Test/Mftf/Test/StorefrontCheckoutTotalsSortOrderInCartTest.xml create mode 100644 app/code/Magento/Checkout/Test/Mftf/Test/StorefrontCustomerCheckoutTestWithRestrictedCountriesForPaymentTest.xml create mode 100644 app/code/Magento/Checkout/Test/Mftf/Test/StorefrontGuestCheckoutDataPersistTest.xml create mode 100644 app/code/Magento/Checkout/Test/Mftf/Test/StorefrontGuestCheckoutTestWithRestrictedCountriesForPaymentTest.xml create mode 100644 app/code/Magento/Checkout/Test/Mftf/Test/StorefrontOnePageCheckoutDataWhenChangeQtyTest.xml create mode 100644 app/code/Magento/Checkout/Test/Mftf/Test/StorefrontShoppingCartCheckCustomerDefaultShippingAddressForVirtualQuoteTest.xml create mode 100644 app/code/Magento/Checkout/Test/Mftf/Test/StorefrontUpdateQtyInShoppingCartAfterUpdateInMinicartTest.xml create mode 100644 app/code/Magento/Checkout/Test/Unit/Block/Checkout/AttributeMergerTest.php create mode 100644 app/code/Magento/Checkout/view/frontend/web/js/view/summary/item/details/message.js create mode 100644 app/code/Magento/Checkout/view/frontend/web/template/summary/item/details/message.html create mode 100644 app/code/Magento/Cms/Test/Mftf/ActionGroup/NavigateToCMSPagesActionGroup.xml create mode 100644 app/code/Magento/Cms/Test/Mftf/Data/CmsBlockData.xml create mode 100644 app/code/Magento/Cms/Test/Mftf/Metadata/cms_block-meta.xml create mode 100644 app/code/Magento/Cms/Test/Mftf/Page/AdminCmsBlockEditPage.xml create mode 100644 app/code/Magento/Cms/Test/Mftf/Page/AdminCmsBlockGridPage.xml create mode 100644 app/code/Magento/Cms/Test/Mftf/Section/AdminCmsBlockContentSection.xml create mode 100644 app/code/Magento/Config/Test/Mftf/Data/WebUrlOptionsConfigData.xml create mode 100644 app/code/Magento/Config/Test/Mftf/Metadata/web_url_options_config-meta.xml create mode 100644 app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdminCheckValidatorConfigurableProductTest.xml create mode 100644 app/code/Magento/ConfigurableProduct/Test/Unit/Plugin/Catalog/Model/Product/Pricing/Renderer/SalableResolverTest.php create mode 100644 app/code/Magento/Contact/view/frontend/web/css/source/_module.less create mode 100644 app/code/Magento/Customer/Block/DataProviders/PostCodesPatternsAttributeData.php create mode 100644 app/code/Magento/Customer/Test/Mftf/ActionGroup/StorefrontCustomerAccountActionGroup.xml create mode 100644 app/code/Magento/Customer/Test/Mftf/ActionGroup/StorefrontCustomerActionGroup.xml create mode 100644 app/code/Magento/Customer/Test/Mftf/Data/CustomerConfigData.xml create mode 100644 app/code/Magento/Customer/Test/Mftf/Metadata/customer_config_account_sharing-meta.xml delete mode 100644 app/code/Magento/Customer/Test/Mftf/Page/StorefrontCustomerOrderPage.xml create mode 100644 app/code/Magento/Customer/Test/Mftf/Page/StorefrontCustomerOrdersGridPage.xml create mode 100644 app/code/Magento/Customer/Test/Mftf/Section/StorefrontCustomerOrdersGridSection.xml create mode 100644 app/code/Magento/GiftMessage/Test/Mftf/Data/GiftOptionsData.xml create mode 100644 app/code/Magento/GiftMessage/Test/Mftf/Metadata/gift_options-meta.xml create mode 100644 app/code/Magento/GiftMessage/Test/Mftf/Section/StorefrontCheckoutGiftOptionsSection.xml create mode 100644 app/code/Magento/GroupedProduct/Test/Mftf/ActionGroup/AdminGroupedProductActionGroup.xml create mode 100644 app/code/Magento/GroupedProduct/Test/Mftf/Page/AdminOrderCreatePage.xml create mode 100644 app/code/Magento/GroupedProduct/Test/Mftf/Page/AdminProductCreatePage.xml create mode 100644 app/code/Magento/GroupedProduct/Test/Mftf/Section/AdminAddProductsToGroupPanelSection.xml create mode 100644 app/code/Magento/GroupedProduct/Test/Mftf/Section/AdminOrderConfigureGroupedProductSection.xml create mode 100644 app/code/Magento/GroupedProduct/Test/Mftf/Section/AdminProductFormGroupedProductsSection.xml create mode 100644 app/code/Magento/GroupedProduct/Test/Mftf/Test/AdminSortingAssociatedProductsTest.xml create mode 100644 app/code/Magento/GroupedProduct/view/adminhtml/web/js/grouped-product-grid.js create mode 100644 app/code/Magento/GroupedProduct/view/adminhtml/web/template/components/position.html create mode 100644 app/code/Magento/ImportExport/Model/Import/SampleFileProvider.php create mode 100644 app/code/Magento/ImportExport/Test/Mftf/Page/AdminImportIndexPage.xml create mode 100644 app/code/Magento/ImportExport/Test/Mftf/Section/AdminImportHeaderSection.xml create mode 100644 app/code/Magento/ImportExport/Test/Mftf/Section/AdminImportMainSection.xml create mode 100644 app/code/Magento/ImportExport/Test/Mftf/Test/AdminImportProductsWithDeleteBehaviorTest.xml create mode 100644 app/code/Magento/ImportExport/Test/Unit/Model/Import/SampleFileProviderTest.php create mode 100644 app/code/Magento/Multishipping/Test/Mftf/Page/StorefrontMultishippingCheckoutAddressesPage.xml create mode 100644 app/code/Magento/Multishipping/Test/Mftf/Page/StorefrontMultishippingCheckoutShippingPage.xml create mode 100644 app/code/Magento/Multishipping/Test/Mftf/Section/StorefrontCheckoutCartSummarySection.xml create mode 100644 app/code/Magento/Multishipping/Test/Mftf/Section/StorefrontMultishippingCheckoutSection.xml create mode 100644 app/code/Magento/Payment/Api/Data/PaymentAdditionalInfoInterface.php create mode 100644 app/code/Magento/Payment/Model/PaymentAdditionalInfo.php create mode 100644 app/code/Magento/Persistent/CustomerData/Persistent.php create mode 100644 app/code/Magento/Persistent/Model/Plugin/PersistentCustomerContext.php create mode 100644 app/code/Magento/Persistent/Test/Mftf/ActionGroup/StorefrontCustomerActionGroup.xml create mode 100644 app/code/Magento/Persistent/Test/Mftf/Section/StorefrontCustomerSignInFormSection.xml create mode 100644 app/code/Magento/Persistent/Test/Mftf/Test/StorefrontCorrectWelcomeMessageAfterCustomerIsLoggedOutTest.xml create mode 100644 app/code/Magento/Persistent/etc/frontend/sections.xml create mode 100644 app/code/Magento/Persistent/view/frontend/requirejs-config.js create mode 100644 app/code/Magento/Persistent/view/frontend/templates/additional.phtml create mode 100644 app/code/Magento/Persistent/view/frontend/web/js/view/additional-welcome.js create mode 100644 app/code/Magento/Persistent/view/frontend/web/js/view/customer-data-mixin.js create mode 100644 app/code/Magento/ProductVideo/Test/Mftf/ActionGroup/AdminProductVideoActionGroup.xml create mode 100644 app/code/Magento/ProductVideo/Test/Mftf/ActionGroup/StorefrontProductVideoActionGroup.xml create mode 100644 app/code/Magento/ProductVideo/Test/Mftf/Data/ProductVideoConfigData.xml create mode 100644 app/code/Magento/ProductVideo/Test/Mftf/Data/ProductVideoData.xml create mode 100644 app/code/Magento/ProductVideo/Test/Mftf/Metadata/product_video_config-meta.xml create mode 100644 app/code/Magento/ProductVideo/Test/Mftf/Page/AdminProductCreatePage.xml create mode 100644 app/code/Magento/ProductVideo/Test/Mftf/Section/AdminProductImagesSection.xml create mode 100644 app/code/Magento/ProductVideo/Test/Mftf/Section/AdminProductNewVideoSection.xml create mode 100644 app/code/Magento/ProductVideo/Test/Mftf/Section/StorefrontProductInfoMainSection.xml create mode 100644 app/code/Magento/ProductVideo/Test/Mftf/Test/StorefrontYoutubeVideoWindowOnProductPageTest.xml create mode 100644 app/code/Magento/Reports/Test/Mftf/Page/AdminReportSalesTaxPage.xml create mode 100644 app/code/Magento/Reports/Test/Mftf/Section/AdminReportMainActionSection.xml create mode 100644 app/code/Magento/Reports/Test/Mftf/Section/AdminReportTaxSection.xml create mode 100644 app/code/Magento/Reports/Test/Mftf/Test/AdminTaxReportGridTest.xml create mode 100644 app/code/Magento/Sales/Model/Order/CustomerAssignment.php create mode 100644 app/code/Magento/Sales/Model/Order/Webapi/ChangeOutputArray.php create mode 100644 app/code/Magento/Sales/Test/Mftf/ActionGroup/AdminInvoiceActionGroup.xml create mode 100644 app/code/Magento/Sales/Test/Mftf/ActionGroup/StorefrontSearchGuestOrderActionGroup.xml create mode 100644 app/code/Magento/Sales/Test/Mftf/Data/ConstData.xml create mode 100644 app/code/Magento/Sales/Test/Mftf/Page/AdminOrderAddressEditPage.xml create mode 100644 app/code/Magento/Sales/Test/Mftf/Page/AdminOrderInvoiceViewPage.xml create mode 100644 app/code/Magento/Sales/Test/Mftf/Page/StorefrontGuestOrderSearchPage.xml create mode 100644 app/code/Magento/Sales/Test/Mftf/Page/StorefrontGuestOrderViewPage.xml create mode 100644 app/code/Magento/Sales/Test/Mftf/Section/AdminInvoiceAddressInformationSection.xml create mode 100644 app/code/Magento/Sales/Test/Mftf/Section/AdminInvoiceOrderAndAccountInformationSection.xml create mode 100644 app/code/Magento/Sales/Test/Mftf/Section/AdminOrderAddressEditSection.xml create mode 100644 app/code/Magento/Sales/Test/Mftf/Section/AdminOrderFormBundleProductSection.xml create mode 100644 app/code/Magento/Sales/Test/Mftf/Section/AdminOrderInvoiceViewMainActionsSection.xml create mode 100644 app/code/Magento/Sales/Test/Mftf/Section/AdminOrderStoreScopeTreeSection.xml create mode 100644 app/code/Magento/Sales/Test/Mftf/Section/StorefrontGuestOrderSearchSection.xml create mode 100644 app/code/Magento/Sales/Test/Mftf/Section/StorefrontGuestOrderViewSection.xml create mode 100644 app/code/Magento/Sales/Test/Mftf/Test/AdminCheckingCreditMemoTotalsTest.xml create mode 100644 app/code/Magento/Sales/Test/Mftf/Test/AdminCreateCreditMemoWhenCartRuleDeletedTest.xml create mode 100644 app/code/Magento/Sales/Test/Mftf/Test/AdminCreateOrderWithBundleProductTest.xml rename app/code/Magento/Sales/Test/Mftf/Test/{CreditMemoTotalAfterShippingDiscountTest.xml => AdminCreditMemoTotalAfterShippingDiscountTest.xml} (91%) create mode 100644 app/code/Magento/Sales/Test/Unit/Controller/Adminhtml/Order/AddCommentTest.php create mode 100644 app/code/Magento/Sales/Test/Unit/Model/Order/Webapi/ChangeOutputArrayTest.php create mode 100644 app/code/Magento/Sales/ViewModel/Customer/AddressFormatter.php create mode 100644 app/code/Magento/SalesRule/Observer/AssignCouponDataAfterOrderCustomerAssignObserver.php create mode 100644 app/code/Magento/SalesRule/Test/Mftf/ActionGroup/AdminEditCartPriceRuleActionGroup.xml create mode 100644 app/code/Magento/SalesRule/Test/Mftf/ActionGroup/AdminFilterCartPriceRuleActionGroup.xml create mode 100644 app/code/Magento/SalesRule/Test/Mftf/ActionGroup/StorefrontSalesRuleActionGroup.xml create mode 100644 app/code/Magento/SalesRule/Test/Mftf/Data/SalesRuleProductConditionsData.xml create mode 100644 app/code/Magento/SalesRule/Test/Mftf/Page/AdminCartPriceRuleEditPage.xml create mode 100644 app/code/Magento/SalesRule/Test/Mftf/Page/AdminCartPriceRulesLimitPage.xml create mode 100644 app/code/Magento/SalesRule/Test/Mftf/Test/AdminCartRulesAppliedForProductInCartTest.xml create mode 100644 app/code/Magento/Shipping/Test/Mftf/ActionGroup/AdminShipmentActionGroup.xml create mode 100644 app/code/Magento/Shipping/Test/Mftf/Section/AdminShipmentAddressInformationSection.xml create mode 100644 app/code/Magento/Shipping/Test/Mftf/Section/AdminShipmentOrderAndAccountInformationSection.xml create mode 100644 app/code/Magento/Store/Test/Mftf/ActionGroup/AdminSwitchStoreViewActionGroup.xml create mode 100644 app/code/Magento/Store/Test/Mftf/ActionGroup/StorefrontSwitchStoreViewActionGroup.xml create mode 100644 app/code/Magento/Store/Test/Mftf/Data/ProductWebsiteLinkData.xml create mode 100644 app/code/Magento/Store/Test/Mftf/Metadata/product_website_link-meta.xml create mode 100644 app/code/Magento/Store/Test/Mftf/Page/StorefrontStoreHomePage.xml create mode 100644 app/code/Magento/Tax/Test/Mftf/ActionGroup/AdminTaxActionGroup.xml create mode 100644 app/code/Magento/Tax/Test/Mftf/Data/TaxClassData.xml create mode 100644 app/code/Magento/Tax/Test/Mftf/Data/TaxRateData.xml create mode 100644 app/code/Magento/Tax/Test/Mftf/Metadata/tax_class-meta.xml create mode 100644 app/code/Magento/Tax/Test/Mftf/Metadata/tax_rate-meta.xml create mode 100644 app/code/Magento/Tax/Test/Mftf/Page/AdminTaxRuleNewPage.xml create mode 100644 app/code/Magento/Tax/Test/Mftf/Page/AdminTaxRulesGridPage.xml create mode 100644 app/code/Magento/Tax/Test/Mftf/Section/AdminTaxRuleEditFormSection.xml create mode 100644 app/code/Magento/Tax/Test/Mftf/Section/AdminTaxRulesGridSection.xml create mode 100644 app/code/Magento/Theme/Test/Mftf/Section/StorefrontHeaderSection.xml create mode 100644 app/code/Magento/Ui/Test/Mftf/Section/StorefrontMessagesSection.xml create mode 100644 app/code/Magento/UrlRewrite/Test/Mftf/Test/AdminCheckUrlRewritesCorrectlyGeneratedForMultipleStoreviewsDuringProductImportTest.xml create mode 100644 app/code/Magento/Wishlist/Test/Mftf/Test/StorefrontCheckAmountLimitWishlistTest.xml create mode 100644 dev/tests/acceptance/tests/_data/catalog_products.csv create mode 100644 dev/tests/acceptance/tests/_data/import_updated.csv create mode 100644 dev/tests/acceptance/tests/_suite/WYSIWYGDisabledSuite.xml create mode 100644 dev/tests/integration/testsuite/Magento/Braintree/Controller/Paypal/PlaceOrderTest.php create mode 100644 dev/tests/integration/testsuite/Magento/Braintree/Fixtures/paypal_quote.php create mode 100644 dev/tests/integration/testsuite/Magento/Braintree/Fixtures/paypal_quote_rollback.php create mode 100644 dev/tests/integration/testsuite/Magento/Catalog/Model/Indexer/Product/Flat/Action/CustomFlatAttributeTest.php create mode 100644 dev/tests/integration/testsuite/Magento/Catalog/Model/RemoveRedundantImageTest.php create mode 100644 dev/tests/integration/testsuite/Magento/Catalog/_files/categories_with_image.php create mode 100644 dev/tests/integration/testsuite/Magento/Catalog/_files/categories_with_image_rollback.php create mode 100644 dev/tests/integration/testsuite/Magento/Catalog/_files/flat_dropdown_attribute.php create mode 100644 dev/tests/integration/testsuite/Magento/Catalog/_files/flat_dropdown_attribute_rollback.php create mode 100644 dev/tests/integration/testsuite/Magento/Catalog/_files/product_simple_with_custom_attribute_in_flat.php create mode 100644 dev/tests/integration/testsuite/Magento/Catalog/_files/product_simple_with_custom_attribute_in_flat_rollback.php create mode 100644 dev/tests/integration/testsuite/Magento/Catalog/_files/products_in_different_stores.php create mode 100644 dev/tests/integration/testsuite/Magento/Catalog/_files/products_in_different_stores_rollback.php create mode 100644 dev/tests/integration/testsuite/Magento/CatalogImportExport/Model/Import/_files/import_media_non_existing_images.csv create mode 100644 dev/tests/integration/testsuite/Magento/CatalogImportExport/Model/Import/_files/product_import_qty_wrong_sku.csv create mode 100644 dev/tests/integration/testsuite/Magento/CatalogImportExport/Model/Import/_files/product_to_import_with_category.csv create mode 100755 dev/tests/integration/testsuite/Magento/CatalogImportExport/Model/Import/_files/products_error_img.csv create mode 100644 dev/tests/integration/testsuite/Magento/CatalogImportExport/Model/Import/_files/products_to_import_with_backorders_disabled_and_not_0_qty.csv create mode 100644 dev/tests/integration/testsuite/Magento/Customer/_files/customer_confirmation_config_disable.php create mode 100644 dev/tests/integration/testsuite/Magento/Customer/_files/customer_confirmation_config_disable_rollback.php create mode 100644 dev/tests/integration/testsuite/Magento/Customer/_files/customer_confirmation_config_enable.php create mode 100644 dev/tests/integration/testsuite/Magento/Customer/_files/customer_confirmation_config_enable_rollback.php create mode 100644 dev/tests/integration/testsuite/Magento/Customer/_files/customer_confirmation_email_address_with_special_chars.php create mode 100644 dev/tests/integration/testsuite/Magento/Customer/_files/customer_confirmation_email_address_with_special_chars_rollback.php create mode 100644 dev/tests/integration/testsuite/Magento/Quote/Model/Quote/Item/UpdaterTest.php create mode 100644 dev/tests/integration/testsuite/Magento/Review/Controller/CaseCheckAddingProductReviewTest.php create mode 100644 dev/tests/integration/testsuite/Magento/Review/_files/disable_config.php create mode 100644 dev/tests/integration/testsuite/Magento/Sales/Controller/Adminhtml/Order/Creditmemo/AbstractCreditmemoControllerTest.php create mode 100644 dev/tests/integration/testsuite/Magento/Sales/Controller/Adminhtml/Order/Creditmemo/AddCommentTest.php create mode 100644 dev/tests/integration/testsuite/Magento/Sales/Controller/Adminhtml/Order/Creditmemo/SaveTest.php create mode 100644 dev/tests/integration/testsuite/Magento/Sales/Controller/Adminhtml/Order/EmailTest.php create mode 100644 dev/tests/integration/testsuite/Magento/Sales/Controller/Adminhtml/Order/Invoice/AbstractInvoiceControllerTest.php create mode 100644 dev/tests/integration/testsuite/Magento/Sales/Controller/Adminhtml/Order/Invoice/AddCommentTest.php create mode 100644 dev/tests/integration/testsuite/Magento/Sales/Controller/Adminhtml/Order/Invoice/EmailTest.php create mode 100644 dev/tests/integration/testsuite/Magento/Sales/Controller/Adminhtml/Order/Invoice/SaveTest.php create mode 100644 dev/tests/integration/testsuite/Magento/Sales/CustomerData/LastOrderedItemsTest.php create mode 100644 dev/tests/integration/testsuite/Magento/Sales/Model/Order/CreateTest.php create mode 100644 dev/tests/integration/testsuite/Magento/Sales/_files/guest_quote_with_addresses.php create mode 100644 dev/tests/integration/testsuite/Magento/Sales/_files/guest_quote_with_addresses_rollback.php create mode 100644 dev/tests/integration/testsuite/Magento/Sales/_files/order_list_with_tax.php create mode 100644 dev/tests/integration/testsuite/Magento/Sales/_files/order_list_with_tax_rollback.php create mode 100644 dev/tests/integration/testsuite/Magento/Sales/_files/order_with_customer_and_multiple_order_items.php create mode 100644 dev/tests/integration/testsuite/Magento/Sales/_files/order_with_discount.php create mode 100644 dev/tests/integration/testsuite/Magento/Sales/_files/order_with_discount_rollback.php create mode 100644 dev/tests/integration/testsuite/Magento/Sales/_files/order_with_multiple_items.php create mode 100644 dev/tests/integration/testsuite/Magento/Sales/_files/order_with_tax.php create mode 100644 dev/tests/integration/testsuite/Magento/Sales/_files/order_with_tax_rollback.php create mode 100644 dev/tests/integration/testsuite/Magento/Sales/_files/quote_with_custom_price.php create mode 100644 dev/tests/integration/testsuite/Magento/Sales/_files/quote_with_custom_price_rollback.php create mode 100644 dev/tests/integration/testsuite/Magento/SalesRule/Model/Observer/AssignCouponDataAfterOrderCustomerAssignTest.php create mode 100644 dev/tests/integration/testsuite/Magento/Shipping/Controller/Adminhtml/Order/Shipment/AbstractShipmentControllerTest.php create mode 100644 dev/tests/integration/testsuite/Magento/Shipping/Controller/Adminhtml/Order/Shipment/AddCommentTest.php create mode 100644 dev/tests/integration/testsuite/Magento/Shipping/Controller/Adminhtml/Order/Shipment/SaveTest.php create mode 100644 lib/internal/Magento/Framework/Serialize/Serializer/JsonHexTag.php create mode 100644 lib/internal/Magento/Framework/Serialize/Test/Unit/Serializer/JsonHexTagTest.php diff --git a/app/code/Magento/Authorizenet/Block/Adminhtml/Order/View/Info/PaymentDetails.php b/app/code/Magento/Authorizenet/Block/Adminhtml/Order/View/Info/PaymentDetails.php new file mode 100644 index 0000000000000..396df76d882e4 --- /dev/null +++ b/app/code/Magento/Authorizenet/Block/Adminhtml/Order/View/Info/PaymentDetails.php @@ -0,0 +1,28 @@ +setIsTransactionPending(true) ->setIsFraudDetected(true); } + + $additionalInformationKeys = explode(',', $this->getValue('paymentInfoKeys')); + foreach ($additionalInformationKeys as $paymentInfoKey) { + $paymentInfoValue = $response->getDataByKey($paymentInfoKey); + if ($paymentInfoValue !== null) { + $payment->setAdditionalInformation($paymentInfoKey, $paymentInfoValue); + } + } } /** @@ -682,6 +689,7 @@ protected function matchAmount($amount) /** * Operate with order using information from Authorize.net. + * * Authorize order or authorize and capture it. * * @param \Magento\Sales\Model\Order $order @@ -824,6 +832,7 @@ protected function declineOrder(\Magento\Sales\Model\Order $order, $message = '' ->void($response); } $order->registerCancellation($message)->save(); + $this->_eventManager->dispatch('order_cancel_after', ['order' => $order ]); } catch (\Exception $e) { //quiet decline $this->getPsrLogger()->critical($e); @@ -858,7 +867,7 @@ public function getConfigInterface() * Getter for specified value according to set payment method code * * @param mixed $key - * @param null $storeId + * @param int|string|null|\Magento\Store\Model\Store $storeId * @return mixed */ public function getValue($key, $storeId = null) @@ -918,10 +927,13 @@ public function fetchTransactionInfo(\Magento\Payment\Model\InfoInterface $payme $payment->setIsTransactionDenied(true); } $this->addStatusCommentOnUpdate($payment, $response, $transactionId); - return []; + + return $response->getData(); } /** + * Add statuc comment on update. + * * @param \Magento\Sales\Model\Order\Payment $payment * @param \Magento\Framework\DataObject $response * @param string $transactionId @@ -996,8 +1008,9 @@ protected function getTransactionResponse($transactionId) } /** - * @return \Psr\Log\LoggerInterface + * Get psr logger. * + * @return \Psr\Log\LoggerInterface * @deprecated 100.1.0 */ private function getPsrLogger() @@ -1038,7 +1051,9 @@ private function getOrderIncrementId(): string } /** - * Checks if filter action is Report Only. Transactions that trigger this filter are processed as normal, + * Checks if filter action is Report Only. + * + * Transactions that trigger this filter are processed as normal, * but are also reported in the Merchant Interface as triggering this filter. * * @param string $fdsFilterAction diff --git a/app/code/Magento/Authorizenet/etc/config.xml b/app/code/Magento/Authorizenet/etc/config.xml index eacf77cda1e77..3a192646b6f7e 100644 --- a/app/code/Magento/Authorizenet/etc/config.xml +++ b/app/code/Magento/Authorizenet/etc/config.xml @@ -32,6 +32,7 @@ https://secure.authorize.net/gateway/transact.dll https://apitest.authorize.net/xml/v1/request.api https://api2.authorize.net/xml/v1/request.api + x_card_type,x_account_number,x_avs_code,x_auth_code,x_response_reason_text,x_cvv2_resp_code diff --git a/app/code/Magento/Authorizenet/etc/di.xml b/app/code/Magento/Authorizenet/etc/di.xml index 4beb2456be110..69d24019f2fb7 100644 --- a/app/code/Magento/Authorizenet/etc/di.xml +++ b/app/code/Magento/Authorizenet/etc/di.xml @@ -35,4 +35,9 @@ + + + Magento\Authorizenet\Model\Directpost + + diff --git a/app/code/Magento/Authorizenet/i18n/en_US.csv b/app/code/Magento/Authorizenet/i18n/en_US.csv index bb59afffff2c6..6228d5102b13c 100644 --- a/app/code/Magento/Authorizenet/i18n/en_US.csv +++ b/app/code/Magento/Authorizenet/i18n/en_US.csv @@ -67,3 +67,9 @@ Debug,Debug "Minimum Order Total","Minimum Order Total" "Maximum Order Total","Maximum Order Total" "Sort Order","Sort Order" +"x_card_type","Credit Card Type" +"x_account_number", "Credit Card Number" +"x_avs_code","AVS Response Code" +"x_auth_code","Processor Authentication Code" +"x_response_reason_text","Processor Response Text" +"x_cvv2_resp_code","CVV2 Response Code" diff --git a/app/code/Magento/Authorizenet/view/adminhtml/templates/order/view/info/fraud_details.phtml b/app/code/Magento/Authorizenet/view/adminhtml/templates/order/view/info/fraud_details.phtml index 60fec263352fe..ac91fa30bfbe0 100644 --- a/app/code/Magento/Authorizenet/view/adminhtml/templates/order/view/info/fraud_details.phtml +++ b/app/code/Magento/Authorizenet/view/adminhtml/templates/order/view/info/fraud_details.phtml @@ -44,8 +44,8 @@ $fraudDetails = $payment->getAdditionalInformation('fraud_details'); - escapeHtml(__('Fraud Filters')) ?>: -
+ escapeHtml(__('Fraud Filters')) ?>: +
escapeHtml($filter['name']) ?>: escapeHtml($filter['action']) ?> diff --git a/app/code/Magento/Backend/Block/Widget/Form/Element/Dependence.php b/app/code/Magento/Backend/Block/Widget/Form/Element/Dependence.php index eff49c3b75ab2..b6efe6edcf211 100644 --- a/app/code/Magento/Backend/Block/Widget/Form/Element/Dependence.php +++ b/app/code/Magento/Backend/Block/Widget/Form/Element/Dependence.php @@ -139,7 +139,7 @@ protected function _toHtml() } /** - * Field dependences JSON map generator + * Field dependencies JSON map generator * @return string */ protected function _getDependsJson() diff --git a/app/code/Magento/Backend/Block/Widget/Grid/Massaction/AbstractMassaction.php b/app/code/Magento/Backend/Block/Widget/Grid/Massaction/AbstractMassaction.php index 99b9bb41ba1a1..ddabeb90921c2 100644 --- a/app/code/Magento/Backend/Block/Widget/Grid/Massaction/AbstractMassaction.php +++ b/app/code/Magento/Backend/Block/Widget/Grid/Massaction/AbstractMassaction.php @@ -6,6 +6,7 @@ namespace Magento\Backend\Block\Widget\Grid\Massaction; use Magento\Backend\Block\Widget\Grid\Massaction\VisibilityCheckerInterface as VisibilityChecker; +use Magento\Framework\Data\Collection\AbstractDb; use Magento\Framework\DataObject; /** @@ -51,7 +52,7 @@ public function __construct( } /** - * @return void + * @inheritdoc */ protected function _construct() { @@ -216,6 +217,7 @@ public function getGridJsObjectName() * Retrieve JSON string of selected checkboxes * * @return string + * @SuppressWarnings(PHPMD.RequestAwareBlockMethod) */ public function getSelectedJson() { @@ -230,6 +232,7 @@ public function getSelectedJson() * Retrieve array of selected checkboxes * * @return string[] + * @SuppressWarnings(PHPMD.RequestAwareBlockMethod) */ public function getSelected() { @@ -251,6 +254,8 @@ public function getApplyButtonHtml() } /** + * Get mass action javascript code. + * * @return string */ public function getJavaScript() @@ -267,6 +272,8 @@ public function getJavaScript() } /** + * Get grid ids in JSON format. + * * @return string */ public function getGridIdsJson() @@ -282,7 +289,11 @@ public function getGridIdsJson() } else { $massActionIdField = $this->getParentBlock()->getMassactionIdField(); } - + if ($allIdsCollection instanceof AbstractDb) { + $allIdsCollection->getSelect()->limit(); + $allIdsCollection->clear(); + } + $gridIds = $allIdsCollection->setPageSize(0)->getColumnValues($massActionIdField); if (!empty($gridIds)) { return join(",", $gridIds); @@ -291,6 +302,8 @@ public function getGridIdsJson() } /** + * Get Html id. + * * @return string */ public function getHtmlId() diff --git a/app/code/Magento/Backend/Model/Search/Customer.php b/app/code/Magento/Backend/Model/Search/Customer.php index 35a7359ce9980..e76a1b77ab2d6 100644 --- a/app/code/Magento/Backend/Model/Search/Customer.php +++ b/app/code/Magento/Backend/Model/Search/Customer.php @@ -89,7 +89,7 @@ public function load() $this->searchCriteriaBuilder->setCurrentPage($this->getStart()); $this->searchCriteriaBuilder->setPageSize($this->getLimit()); - $searchFields = ['firstname', 'lastname', 'company']; + $searchFields = ['firstname', 'lastname', 'billing_company']; $filters = []; foreach ($searchFields as $field) { $filters[] = $this->filterBuilder diff --git a/app/code/Magento/Backend/Test/Mftf/Section/AdminMainActionsSection.xml b/app/code/Magento/Backend/Test/Mftf/Section/AdminMainActionsSection.xml index bba375c2d6bfd..06d4a508acdd9 100644 --- a/app/code/Magento/Backend/Test/Mftf/Section/AdminMainActionsSection.xml +++ b/app/code/Magento/Backend/Test/Mftf/Section/AdminMainActionsSection.xml @@ -12,5 +12,6 @@ + diff --git a/app/code/Magento/Backend/Test/Mftf/Section/AdminMessagesSection.xml b/app/code/Magento/Backend/Test/Mftf/Section/AdminMessagesSection.xml index 4af66986d9aa8..5040a08967fa3 100644 --- a/app/code/Magento/Backend/Test/Mftf/Section/AdminMessagesSection.xml +++ b/app/code/Magento/Backend/Test/Mftf/Section/AdminMessagesSection.xml @@ -6,11 +6,13 @@ */ --> - +
+
diff --git a/app/code/Magento/Backend/Test/Mftf/Section/AdminSlideOutDialogSection.xml b/app/code/Magento/Backend/Test/Mftf/Section/AdminSlideOutDialogSection.xml new file mode 100644 index 0000000000000..a2645c9cbf96d --- /dev/null +++ b/app/code/Magento/Backend/Test/Mftf/Section/AdminSlideOutDialogSection.xml @@ -0,0 +1,17 @@ + + + + +
+ + + + +
+
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 8e30afdf51f7f..b4bc42b95d0aa 100644 --- a/app/code/Magento/Backend/view/adminhtml/templates/system/cache/additional.phtml +++ b/app/code/Magento/Backend/view/adminhtml/templates/system/cache/additional.phtml @@ -11,10 +11,10 @@ $permissions = $block->getData('permissions'); ?> hasAccessToAdditionalActions()): ?>
+

+ escapeHtml(__('Additional Cache Management')); ?> +

hasAccessToFlushCatalogImages()): ?> -

- escapeHtml(__('Additional Cache Management')); ?> -

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 b220e2c98d77c..46db8a9907341 100644 --- a/app/code/Magento/Bundle/Block/Adminhtml/Catalog/Product/Composite/Fieldset/Options/Type/Checkbox.php +++ b/app/code/Magento/Bundle/Block/Adminhtml/Catalog/Product/Composite/Fieldset/Options/Type/Checkbox.php @@ -20,9 +20,7 @@ class Checkbox extends \Magento\Bundle\Block\Catalog\Product\View\Type\Bundle\Op protected $_template = 'Magento_Bundle::product/composite/fieldset/options/type/checkbox.phtml'; /** - * @param string $elementId - * @param string $containerId - * @return string + * @inheritdoc */ public function setValidationContainer($elementId, $containerId) { @@ -34,4 +32,15 @@ public function setValidationContainer($elementId, $containerId) '\'; '; } + + /** + * @inheritdoc + */ + public function getSelectionPrice($selection) + { + $price = parent::getSelectionPrice($selection); + $qty = $selection->getSelectionQty(); + + return $price * $qty; + } } diff --git a/app/code/Magento/Bundle/Block/Adminhtml/Catalog/Product/Composite/Fieldset/Options/Type/Multi.php b/app/code/Magento/Bundle/Block/Adminhtml/Catalog/Product/Composite/Fieldset/Options/Type/Multi.php index a4b8c6bde73aa..629f08dc75106 100644 --- a/app/code/Magento/Bundle/Block/Adminhtml/Catalog/Product/Composite/Fieldset/Options/Type/Multi.php +++ b/app/code/Magento/Bundle/Block/Adminhtml/Catalog/Product/Composite/Fieldset/Options/Type/Multi.php @@ -20,9 +20,7 @@ class Multi extends \Magento\Bundle\Block\Catalog\Product\View\Type\Bundle\Optio protected $_template = 'Magento_Bundle::product/composite/fieldset/options/type/multi.phtml'; /** - * @param string $elementId - * @param string $containerId - * @return string + * @inheritdoc */ public function setValidationContainer($elementId, $containerId) { @@ -34,4 +32,15 @@ public function setValidationContainer($elementId, $containerId) '\'; '; } + + /** + * @inheritdoc + */ + public function getSelectionPrice($selection) + { + $price = parent::getSelectionPrice($selection); + $qty = $selection->getSelectionQty(); + + return $price * $qty; + } } diff --git a/app/code/Magento/Bundle/Pricing/Adjustment/Calculator.php b/app/code/Magento/Bundle/Pricing/Adjustment/Calculator.php index adb0777151b9e..2f0a99072594b 100644 --- a/app/code/Magento/Bundle/Pricing/Adjustment/Calculator.php +++ b/app/code/Magento/Bundle/Pricing/Adjustment/Calculator.php @@ -281,7 +281,7 @@ public function calculateBundleAmount($basePriceValue, $bundleProduct, $selectio * @param float $basePriceValue * @param Product $bundleProduct * @param \Magento\Bundle\Pricing\Price\BundleSelectionPrice[] $selectionPriceList - * @param null|bool|string|arrayy $exclude + * @param null|bool|string|array $exclude * @return \Magento\Framework\Pricing\Amount\AmountInterface */ protected function calculateFixedBundleAmount($basePriceValue, $bundleProduct, $selectionPriceList, $exclude) diff --git a/app/code/Magento/Bundle/Test/Mftf/ActionGroup/BundleProductsOnAdminActionGroup.xml b/app/code/Magento/Bundle/Test/Mftf/ActionGroup/BundleProductsOnAdminActionGroup.xml index 231ae13cab2b1..c600e80f7265f 100644 --- a/app/code/Magento/Bundle/Test/Mftf/ActionGroup/BundleProductsOnAdminActionGroup.xml +++ b/app/code/Magento/Bundle/Test/Mftf/ActionGroup/BundleProductsOnAdminActionGroup.xml @@ -51,4 +51,14 @@ + + + + + + + + + + diff --git a/app/code/Magento/Bundle/Test/Mftf/Page/StorefrontProductPage.xml b/app/code/Magento/Bundle/Test/Mftf/Page/StorefrontProductPage.xml index a495b6be183ba..89cc6a6d5d2fe 100644 --- a/app/code/Magento/Bundle/Test/Mftf/Page/StorefrontProductPage.xml +++ b/app/code/Magento/Bundle/Test/Mftf/Page/StorefrontProductPage.xml @@ -7,7 +7,7 @@ --> + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Page/etc/PageObject.xsd">
diff --git a/app/code/Magento/Bundle/Test/Mftf/Section/AdminProductFormBundleSection.xml b/app/code/Magento/Bundle/Test/Mftf/Section/AdminProductFormBundleSection.xml index a843b942174d5..8f60227926099 100644 --- a/app/code/Magento/Bundle/Test/Mftf/Section/AdminProductFormBundleSection.xml +++ b/app/code/Magento/Bundle/Test/Mftf/Section/AdminProductFormBundleSection.xml @@ -16,5 +16,8 @@ + + +
diff --git a/app/code/Magento/Bundle/Test/Mftf/Section/StorefrontProductInfoMainSection.xml b/app/code/Magento/Bundle/Test/Mftf/Section/StorefrontProductInfoMainSection.xml index 41c00b5eda184..b1acc97cc0261 100644 --- a/app/code/Magento/Bundle/Test/Mftf/Section/StorefrontProductInfoMainSection.xml +++ b/app/code/Magento/Bundle/Test/Mftf/Section/StorefrontProductInfoMainSection.xml @@ -7,11 +7,12 @@ --> + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Page/etc/SectionObject.xsd">
+
diff --git a/app/code/Magento/Bundle/view/frontend/templates/sales/order/items/renderer.phtml b/app/code/Magento/Bundle/view/frontend/templates/sales/order/items/renderer.phtml index 063d66edb9e70..74e1c5f874954 100644 --- a/app/code/Magento/Bundle/view/frontend/templates/sales/order/items/renderer.phtml +++ b/app/code/Magento/Bundle/view/frontend/templates/sales/order/items/renderer.phtml @@ -7,95 +7,111 @@ // @codingStandardsIgnoreFile /** @var $block \Magento\Bundle\Block\Sales\Order\Items\Renderer */ +$parentItem = $block->getItem(); +$items = array_merge([$parentItem], $parentItem->getChildrenItems()); +$index = 0; +$prevOptionId = ''; ?> -getItem() ?> -getChildrenItems()); ?> - - + - - - getItemOptions() || $parentItem->getDescription() || $this->helper('Magento\GiftMessage\Helper\Message')->isMessagesAllowed('order_item', $parentItem) && $parentItem->getGiftMessageId()): ?> - + getItemOptions() + || $parentItem->getDescription() + || $this->helper('Magento\GiftMessage\Helper\Message')->isMessagesAllowed('order_item', $parentItem) + && $parentItem->getGiftMessageId()): ?> + - + - getParentItem()): ?> - getSelectionAttributes($_item) ?> - + getParentItem()): ?> + getSelectionAttributes($item) ?> + - + escapeHtml($attributes['option_label']); ?> - + -getParentItem()): ?> data-th=""> - getParentItem()): ?> - - escapeHtml($_item->getName()) ?> +getParentItem()): ?> + data-th="escapeHtml($attributes['option_label']); ?>" + > + getParentItem()): ?> + + escapeHtml($item->getName()); ?> - getValueHtml($_item) ?> + + getValueHtml($item); ?> + - prepareSku($_item->getSku()) ?> - - getParentItem()): ?> - getItemPriceHtml() ?> + + prepareSku($item->getSku()); ?> + + + getParentItem()): ?> + getItemPriceHtml(); ?>   - + getParentItem() && $block->isChildCalculated()) || - (!$_item->getParentItem() && !$block->isChildCalculated()) || ($_item->getQtyShipped() > 0 && $_item->getParentItem() && $block->isShipmentSeparately())):?> + ($item->getParentItem() && $block->isChildCalculated()) || + (!$item->getParentItem() && !$block->isChildCalculated()) || + ($item->getQtyShipped() > 0 && $item->getParentItem() && $block->isShipmentSeparately())): ?>
    - getParentItem() && $block->isChildCalculated()) || - (!$_item->getParentItem() && !$block->isChildCalculated())): ?> - getQtyOrdered() > 0): ?> + getParentItem() && $block->isChildCalculated()) || + (!$item->getParentItem() && !$block->isChildCalculated())): ?> + getQtyOrdered() > 0): ?>
  • - - getQtyOrdered()*1 ?> + escapeHtml(__('Ordered')); ?> + getQtyOrdered() * 1; ?>
  • - getQtyShipped() > 0 && !$block->isShipmentSeparately()): ?> + getQtyShipped() > 0 && !$block->isShipmentSeparately()): ?>
  • - - getQtyShipped()*1 ?> + escapeHtml(__('Shipped')); ?> + getQtyShipped() * 1; ?>
  • - getQtyCanceled() > 0): ?> + getQtyCanceled() > 0): ?>
  • - - getQtyCanceled()*1 ?> + escapeHtml(__('Canceled')); ?> + getQtyCanceled() * 1; ?>
  • - getQtyRefunded() > 0): ?> + getQtyRefunded() > 0): ?>
  • - - getQtyRefunded()*1 ?> + escapeHtml(__('Refunded')); ?> + getQtyRefunded() * 1; ?>
  • - getQtyShipped() > 0 && $_item->getParentItem() && $block->isShipmentSeparately()): ?> + getQtyShipped() > 0 && $item->getParentItem() && $block->isShipmentSeparately()): ?>
  • - - getQtyShipped()*1 ?> + escapeHtml(__('Shipped')); ?> + getQtyShipped() * 1; ?>
  • -   + getQtyOrdered() * 1; ?> getParentItem() && $block->isChildCalculated()) || - (!$_item->getParentItem() && !$block->isChildCalculated()) || ($_item->getQtyShipped() > 0 && $_item->getParentItem() && $block->isShipmentSeparately())):?> + ($item->getParentItem() && $block->isChildCalculated()) || + (!$item->getParentItem() && !$block->isChildCalculated()) || + ($item->getQtyShipped() > 0 && $item->getParentItem() && $block->isShipmentSeparately())):?>
- getParentItem()): ?> - getItemRowTotalHtml() ?> + getParentItem()): ?> + getItemRowTotalHtml(); ?>   @@ -103,33 +119,38 @@ -getItemOptions()) || $block->escapeHtml($_item->getDescription()))): ?> +getItemOptions()) || $block->escapeHtml($item->getDescription()))): ?> - getItemOptions()): ?> + getItemOptions()): ?>
- -
escapeHtml($_option['label']) ?>
+ +
escapeHtml($option['label']) ?>
getPrintStatus()): ?> - getFormatedOptionValue($_option) ?> - class="tooltip wrapper"> - - + getFormatedOptionValue($option) ?> + + class="tooltip wrapper" + > + +
-
escapeHtml($_option['label']) ?>
-
+
escapeHtml($option['label']); ?>
+
-
escapeHtml((isset($_option['print_value']) ? $_option['print_value'] : $_option['value'])) ?>
+
escapeHtml((isset($option['print_value']) ? + $option['print_value'] : + $option['value'])); ?> +
- escapeHtml($_item->getDescription()) ?> + escapeHtml($item->getDescription()); ?> diff --git a/app/code/Magento/BundleImportExport/Test/Unit/Model/Import/Product/Type/BundleTest.php b/app/code/Magento/BundleImportExport/Test/Unit/Model/Import/Product/Type/BundleTest.php index 773fa6a5349a5..59a704d5305e4 100644 --- a/app/code/Magento/BundleImportExport/Test/Unit/Model/Import/Product/Type/BundleTest.php +++ b/app/code/Magento/BundleImportExport/Test/Unit/Model/Import/Product/Type/BundleTest.php @@ -242,7 +242,7 @@ public function testSaveData($skus, $bunch, $allowImport) 'price_type' => 'fixed', 'shipment_type' => '1', 'default_qty' => '1', - 'is_defaul' => '1', + 'is_default' => '1', 'position' => '1', 'option_id' => '1'] ] @@ -264,7 +264,7 @@ public function testSaveData($skus, $bunch, $allowImport) 'price_type' => 'percent', 'shipment_type' => 0, 'default_qty' => '2', - 'is_defaul' => '1', + 'is_default' => '1', 'position' => '6', 'option_id' => '6'] ] @@ -324,7 +324,7 @@ public function saveDataProvider() . 'price_type=fixed,' . 'shipment_type=separately,' . 'default_qty=1,' - . 'is_defaul=1,' + . 'is_default=1,' . 'position=1,' . 'option_id=1 | name=Bundle2,' . 'type=dropdown,' @@ -333,7 +333,7 @@ public function saveDataProvider() . 'price=10,' . 'price_type=fixed,' . 'default_qty=1,' - . 'is_defaul=1,' + . 'is_default=1,' . 'position=2,' . 'option_id=2' ], diff --git a/app/code/Magento/CacheInvalidate/Model/PurgeCache.php b/app/code/Magento/CacheInvalidate/Model/PurgeCache.php index 8acf170d43cfb..8e0c00b587460 100644 --- a/app/code/Magento/CacheInvalidate/Model/PurgeCache.php +++ b/app/code/Magento/CacheInvalidate/Model/PurgeCache.php @@ -7,6 +7,9 @@ use Magento\Framework\Cache\InvalidateLogger; +/** + * Purge cache action. + */ class PurgeCache { const HEADER_X_MAGENTO_TAGS_PATTERN = 'X-Magento-Tags-Pattern'; @@ -26,6 +29,18 @@ class PurgeCache */ private $logger; + /** + * Batch size of the purge request. + * + * Based on default Varnish 4 http_req_hdr_len size minus a 512 bytes margin for method, + * header name, line feeds etc. + * + * @see https://varnish-cache.org/docs/4.1/reference/varnishd.html + * + * @var int + */ + private $requestSize = 7680; + /** * Constructor * @@ -44,18 +59,68 @@ public function __construct( } /** - * Send curl purge request - * to invalidate cache by tags pattern + * Send curl purge request to invalidate cache by tags pattern. * * @param string $tagsPattern * @return bool Return true if successful; otherwise return false */ public function sendPurgeRequest($tagsPattern) { + $successful = true; $socketAdapter = $this->socketAdapterFactory->create(); $servers = $this->cacheServer->getUris(); - $headers = [self::HEADER_X_MAGENTO_TAGS_PATTERN => $tagsPattern]; $socketAdapter->setOptions(['timeout' => 10]); + + $formattedTagsChunks = $this->splitTags($tagsPattern); + foreach ($formattedTagsChunks as $formattedTagsChunk) { + if (!$this->sendPurgeRequestToServers($socketAdapter, $servers, $formattedTagsChunk)) { + $successful = false; + } + } + + return $successful; + } + + /** + * Split tags by batches + * + * @param string $tagsPattern + * @return \Generator + */ + private function splitTags(string $tagsPattern) : \Generator + { + $tagsBatchSize = 0; + $formattedTagsChunk = []; + $formattedTags = explode('|', $tagsPattern); + foreach ($formattedTags as $formattedTag) { + if ($tagsBatchSize + strlen($formattedTag) > $this->requestSize - count($formattedTagsChunk) - 1) { + yield implode('|', $formattedTagsChunk); + $formattedTagsChunk = []; + $tagsBatchSize = 0; + } + + $tagsBatchSize += strlen($formattedTag); + $formattedTagsChunk[] = $formattedTag; + } + if (!empty($formattedTagsChunk)) { + yield implode('|', $formattedTagsChunk); + } + } + + /** + * Send curl purge request to servers to invalidate cache by tags pattern. + * + * @param \Zend\Http\Client\Adapter\Socket $socketAdapter + * @param \Zend\Uri\Uri[] $servers + * @param string $formattedTagsChunk + * @return bool Return true if successful; otherwise return false + */ + private function sendPurgeRequestToServers( + \Zend\Http\Client\Adapter\Socket $socketAdapter, + array $servers, + string $formattedTagsChunk + ): bool { + $headers = [self::HEADER_X_MAGENTO_TAGS_PATTERN => $formattedTagsChunk]; foreach ($servers as $server) { $headers['Host'] = $server->getHost(); try { @@ -69,12 +134,13 @@ public function sendPurgeRequest($tagsPattern) $socketAdapter->read(); $socketAdapter->close(); } catch (\Exception $e) { - $this->logger->critical($e->getMessage(), compact('server', 'tagsPattern')); + $this->logger->critical($e->getMessage(), compact('server', 'formattedTagsChunk')); + return false; } } + $this->logger->execute(compact('servers', 'formattedTagsChunk')); - $this->logger->execute(compact('servers', 'tagsPattern')); return true; } } diff --git a/app/code/Magento/Catalog/Block/Product/ProductList/Toolbar.php b/app/code/Magento/Catalog/Block/Product/ProductList/Toolbar.php index 674b20a49c837..30811150af143 100644 --- a/app/code/Magento/Catalog/Block/Product/ProductList/Toolbar.php +++ b/app/code/Magento/Catalog/Block/Product/ProductList/Toolbar.php @@ -7,6 +7,10 @@ use Magento\Catalog\Helper\Product\ProductList; use Magento\Catalog\Model\Product\ProductList\Toolbar as ToolbarModel; +use Magento\Catalog\Model\Product\ProductList\ToolbarMemorizer; +use Magento\Framework\App\ObjectManager; +use Magento\Framework\App\Http\Context; +use Magento\Framework\Data\Form\FormKey; /** * Product list toolbar @@ -77,6 +81,7 @@ class Toolbar extends \Magento\Framework\View\Element\Template /** * @var bool $_paramsMemorizeAllowed + * @deprecated */ protected $_paramsMemorizeAllowed = true; @@ -96,6 +101,7 @@ class Toolbar extends \Magento\Framework\View\Element\Template * Catalog session * * @var \Magento\Catalog\Model\Session + * @deprecated */ protected $_catalogSession; @@ -104,6 +110,11 @@ class Toolbar extends \Magento\Framework\View\Element\Template */ protected $_toolbarModel; + /** + * @var ToolbarMemorizer + */ + private $toolbarMemorizer; + /** * @var ProductList */ @@ -119,6 +130,16 @@ class Toolbar extends \Magento\Framework\View\Element\Template */ protected $_postDataHelper; + /** + * @var Context + */ + private $httpContext; + + /** + * @var FormKey + */ + private $formKey; + /** * @param \Magento\Framework\View\Element\Template\Context $context * @param \Magento\Catalog\Model\Session $catalogSession @@ -128,6 +149,11 @@ class Toolbar extends \Magento\Framework\View\Element\Template * @param ProductList $productListHelper * @param \Magento\Framework\Data\Helper\PostHelper $postDataHelper * @param array $data + * @param ToolbarMemorizer|null $toolbarMemorizer + * @param Context|null $httpContext + * @param FormKey|null $formKey + * + * @SuppressWarnings(PHPMD.ExcessiveParameterList) */ public function __construct( \Magento\Framework\View\Element\Template\Context $context, @@ -137,7 +163,10 @@ public function __construct( \Magento\Framework\Url\EncoderInterface $urlEncoder, ProductList $productListHelper, \Magento\Framework\Data\Helper\PostHelper $postDataHelper, - array $data = [] + array $data = [], + ToolbarMemorizer $toolbarMemorizer = null, + Context $httpContext = null, + FormKey $formKey = null ) { $this->_catalogSession = $catalogSession; $this->_catalogConfig = $catalogConfig; @@ -145,6 +174,15 @@ public function __construct( $this->urlEncoder = $urlEncoder; $this->_productListHelper = $productListHelper; $this->_postDataHelper = $postDataHelper; + $this->toolbarMemorizer = $toolbarMemorizer ?: ObjectManager::getInstance()->get( + ToolbarMemorizer::class + ); + $this->httpContext = $httpContext ?: ObjectManager::getInstance()->get( + Context::class + ); + $this->formKey = $formKey ?: ObjectManager::getInstance()->get( + FormKey::class + ); parent::__construct($context, $data); } @@ -152,6 +190,7 @@ public function __construct( * Disable list state params memorizing * * @return $this + * @deprecated */ public function disableParamsMemorizing() { @@ -165,6 +204,7 @@ public function disableParamsMemorizing() * @param string $param parameter name * @param mixed $value parameter value * @return $this + * @deprecated */ protected function _memorizeParam($param, $value) { @@ -244,13 +284,13 @@ public function getCurrentOrder() $defaultOrder = $keys[0]; } - $order = $this->_toolbarModel->getOrder(); + $order = $this->toolbarMemorizer->getOrder(); if (!$order || !isset($orders[$order])) { $order = $defaultOrder; } - if ($order != $defaultOrder) { - $this->_memorizeParam('sort_order', $order); + if ($this->toolbarMemorizer->isMemorizingAllowed()) { + $this->httpContext->setValue(ToolbarModel::ORDER_PARAM_NAME, $order, $defaultOrder); } $this->setData('_current_grid_order', $order); @@ -270,13 +310,13 @@ public function getCurrentDirection() } $directions = ['asc', 'desc']; - $dir = strtolower($this->_toolbarModel->getDirection()); + $dir = strtolower($this->toolbarMemorizer->getDirection()); if (!$dir || !in_array($dir, $directions)) { $dir = $this->_direction; } - if ($dir != $this->_direction) { - $this->_memorizeParam('sort_direction', $dir); + if ($this->toolbarMemorizer->isMemorizingAllowed()) { + $this->httpContext->setValue(ToolbarModel::DIRECTION_PARAM_NAME, $dir, $this->_direction); } $this->setData('_current_grid_direction', $dir); @@ -392,6 +432,8 @@ public function getPagerUrl($params = []) } /** + * Get pager encoded url. + * * @param array $params * @return string */ @@ -412,11 +454,15 @@ public function getCurrentMode() return $mode; } $defaultMode = $this->_productListHelper->getDefaultViewMode($this->getModes()); - $mode = $this->_toolbarModel->getMode(); + $mode = $this->toolbarMemorizer->getMode(); if (!$mode || !isset($this->_availableMode[$mode])) { $mode = $defaultMode; } + if ($this->toolbarMemorizer->isMemorizingAllowed()) { + $this->httpContext->setValue(ToolbarModel::MODE_PARAM_NAME, $mode, $defaultMode); + } + $this->setData('_current_grid_mode', $mode); return $mode; } @@ -568,13 +614,13 @@ public function getLimit() $defaultLimit = $keys[0]; } - $limit = $this->_toolbarModel->getLimit(); + $limit = $this->toolbarMemorizer->getLimit(); if (!$limit || !isset($limits[$limit])) { $limit = $defaultLimit; } - if ($limit != $defaultLimit) { - $this->_memorizeParam('limit_page', $limit); + if ($this->toolbarMemorizer->isMemorizingAllowed()) { + $this->httpContext->setValue(ToolbarModel::LIMIT_PARAM_NAME, $limit, $defaultLimit); } $this->setData('_current_limit', $limit); @@ -582,6 +628,8 @@ public function getLimit() } /** + * Check if limit is current used in toolbar. + * * @param int $limit * @return bool */ @@ -591,6 +639,8 @@ public function isLimitCurrent($limit) } /** + * Pager number of items from which products started on current page. + * * @return int */ public function getFirstNum() @@ -600,6 +650,8 @@ public function getFirstNum() } /** + * Pager number of items products finished on current page. + * * @return int */ public function getLastNum() @@ -609,6 +661,8 @@ public function getLastNum() } /** + * Total number of products in current category. + * * @return int */ public function getTotalNum() @@ -617,6 +671,8 @@ public function getTotalNum() } /** + * Check if current page is the first. + * * @return bool */ public function isFirstPage() @@ -625,6 +681,8 @@ public function isFirstPage() } /** + * Return last page number. + * * @return int */ public function getLastPageNum() @@ -692,6 +750,8 @@ public function getWidgetOptionsJson(array $customOptions = []) 'orderDefault' => $this->getOrderField(), 'limitDefault' => $this->_productListHelper->getDefaultLimitPerPageValue($defaultMode), 'url' => $this->getPagerUrl(), + 'formKey' => $this->formKey->getFormKey(), + 'post' => $this->toolbarMemorizer->isMemorizingAllowed() ? true : false, ]; $options = array_replace_recursive($options, $customOptions); return json_encode(['productListToolbarForm' => $options]); diff --git a/app/code/Magento/Catalog/Controller/Adminhtml/Product/Attribute/Validate.php b/app/code/Magento/Catalog/Controller/Adminhtml/Product/Attribute/Validate.php index e56428a1ae77e..3a81b4633b2ff 100644 --- a/app/code/Magento/Catalog/Controller/Adminhtml/Product/Attribute/Validate.php +++ b/app/code/Magento/Catalog/Controller/Adminhtml/Product/Attribute/Validate.php @@ -100,7 +100,7 @@ public function execute() $attributeCode ); - if ($attribute->getId() && !$attributeId) { + if ($attribute->getId() && !$attributeId || $attributeCode === 'product_type') { $message = strlen($this->getRequest()->getParam('attribute_code')) ? __('An attribute with this code already exists.') : __('An attribute with the same code (%1) already exists.', $attributeCode); diff --git a/app/code/Magento/Catalog/Controller/Category/View.php b/app/code/Magento/Catalog/Controller/Category/View.php index e31b1a17ecdc3..c7d237da2c4b8 100644 --- a/app/code/Magento/Catalog/Controller/Category/View.php +++ b/app/code/Magento/Catalog/Controller/Category/View.php @@ -8,13 +8,17 @@ use Magento\Catalog\Api\CategoryRepositoryInterface; use Magento\Catalog\Model\Layer\Resolver; +use Magento\Catalog\Model\Product\ProductList\ToolbarMemorizer; use Magento\Framework\Exception\NoSuchEntityException; use Magento\Framework\View\Result\PageFactory; +use Magento\Framework\App\Action\Action; /** + * View a category on storefront. Needs to be accessible by POST because of the store switching. + * * @SuppressWarnings(PHPMD.CouplingBetweenObjects) */ -class View extends \Magento\Framework\App\Action\Action +class View extends Action { /** * Core registry @@ -69,6 +73,11 @@ class View extends \Magento\Framework\App\Action\Action */ protected $categoryRepository; + /** + * @var ToolbarMemorizer + */ + private $toolbarMemorizer; + /** * Constructor * @@ -82,6 +91,7 @@ class View extends \Magento\Framework\App\Action\Action * @param \Magento\Framework\Controller\Result\ForwardFactory $resultForwardFactory * @param Resolver $layerResolver * @param CategoryRepositoryInterface $categoryRepository + * @param ToolbarMemorizer|null $toolbarMemorizer * @SuppressWarnings(PHPMD.ExcessiveParameterList) */ public function __construct( @@ -94,7 +104,8 @@ public function __construct( PageFactory $resultPageFactory, \Magento\Framework\Controller\Result\ForwardFactory $resultForwardFactory, Resolver $layerResolver, - CategoryRepositoryInterface $categoryRepository + CategoryRepositoryInterface $categoryRepository, + ToolbarMemorizer $toolbarMemorizer = null ) { parent::__construct($context); $this->_storeManager = $storeManager; @@ -106,6 +117,7 @@ public function __construct( $this->resultForwardFactory = $resultForwardFactory; $this->layerResolver = $layerResolver; $this->categoryRepository = $categoryRepository; + $this->toolbarMemorizer = $toolbarMemorizer ?: $context->getObjectManager()->get(ToolbarMemorizer::class); } /** @@ -130,6 +142,7 @@ protected function _initCategory() } $this->_catalogSession->setLastVisitedCategoryId($category->getId()); $this->_coreRegistry->register('current_category', $category); + $this->toolbarMemorizer->memorizeParams(); try { $this->_eventManager->dispatch( 'catalog_controller_category_init_after', @@ -195,7 +208,7 @@ public function execute() if ($layoutUpdates && is_array($layoutUpdates)) { foreach ($layoutUpdates as $layoutUpdate) { $page->addUpdate($layoutUpdate); - $page->addPageLayoutHandles(['layout_update' => md5($layoutUpdate)], null, false); + $page->addPageLayoutHandles(['layout_update' => sha1($layoutUpdate)], null, false); } } diff --git a/app/code/Magento/Catalog/Model/Category/Product/PositionResolver.php b/app/code/Magento/Catalog/Model/Category/Product/PositionResolver.php index 97941f2d23b9f..edaac39864c5a 100644 --- a/app/code/Magento/Catalog/Model/Category/Product/PositionResolver.php +++ b/app/code/Magento/Catalog/Model/Category/Product/PositionResolver.php @@ -52,6 +52,8 @@ public function getPositions(int $categoryId) $categoryId )->order( 'ccp.position ' . \Magento\Framework\DB\Select::SQL_ASC + )->order( + 'ccp.product_id ' . \Magento\Framework\DB\Select::SQL_DESC ); return array_flip($connection->fetchCol($select)); diff --git a/app/code/Magento/Catalog/Model/Indexer/Product/Flat/Action/Indexer.php b/app/code/Magento/Catalog/Model/Indexer/Product/Flat/Action/Indexer.php index f95807f615390..25a9eebb5a0a9 100644 --- a/app/code/Magento/Catalog/Model/Indexer/Product/Flat/Action/Indexer.php +++ b/app/code/Magento/Catalog/Model/Indexer/Product/Flat/Action/Indexer.php @@ -9,6 +9,7 @@ use Magento\Catalog\Api\Data\ProductInterface; use Magento\Framework\App\ResourceConnection; use Magento\Framework\EntityManager\MetadataPool; +use Magento\Store\Model\Store; /** * Class Indexer @@ -84,7 +85,7 @@ public function write($storeId, $productId, $valueFieldSuffix = '') [ 'entity_id' => 'e.entity_id', 'attribute_id' => 't.attribute_id', - 'value' => $this->_connection->getIfNullSql('`t2`.`value`', '`t`.`value`'), + 'value' => 't.value' ] ); @@ -99,32 +100,30 @@ public function write($storeId, $productId, $valueFieldSuffix = '') sprintf('e.%s = t.%s ', $linkField, $linkField) . $this->_connection->quoteInto( ' AND t.attribute_id IN (?)', array_keys($ids) - ) . ' AND t.store_id = 0', - [] - )->joinLeft( - ['t2' => $tableName], - sprintf('t.%s = t2.%s ', $linkField, $linkField) . - ' AND t.attribute_id = t2.attribute_id ' . - $this->_connection->quoteInto( - ' AND t2.store_id = ?', - $storeId - ), + ) . ' AND ' . $this->_connection->quoteInto('t.store_id IN(?)', [ + Store::DEFAULT_STORE_ID, + $storeId + ]), [] )->where( 'e.entity_id = ' . $productId - ); + )->order('t.store_id ASC'); $cursor = $this->_connection->query($select); while ($row = $cursor->fetch(\Zend_Db::FETCH_ASSOC)) { $updateData[$ids[$row['attribute_id']]] = $row['value']; $valueColumnName = $ids[$row['attribute_id']] . $valueFieldSuffix; if (isset($describe[$valueColumnName])) { - $valueColumns[$row['value']] = $valueColumnName; + $valueColumns[$row['attribute_id']] = [ + 'value' => $row['value'], + 'column_name' => $valueColumnName + ]; } } //Update not simple attributes (eg. dropdown) if (!empty($valueColumns)) { - $valueIds = array_keys($valueColumns); + $valueIds = array_column($valueColumns, 'value'); + $optionIdToAttributeName = array_column($valueColumns, 'column_name', 'value'); $select = $this->_connection->select()->from( ['t' => $this->_productIndexerHelper->getTable('eav_attribute_option_value')], @@ -133,14 +132,14 @@ public function write($storeId, $productId, $valueFieldSuffix = '') $this->_connection->quoteInto('t.option_id IN (?)', $valueIds) )->where( $this->_connection->quoteInto('t.store_id IN(?)', [ - \Magento\Store\Model\Store::DEFAULT_STORE_ID, + Store::DEFAULT_STORE_ID, $storeId ]) ) ->order('t.store_id ASC'); $cursor = $this->_connection->query($select); while ($row = $cursor->fetch(\Zend_Db::FETCH_ASSOC)) { - $valueColumnName = $valueColumns[$row['option_id']]; + $valueColumnName = $optionIdToAttributeName[$row['option_id']]; if (isset($describe[$valueColumnName])) { $updateData[$valueColumnName] = $row['value']; } @@ -150,6 +149,7 @@ public function write($storeId, $productId, $valueFieldSuffix = '') $columnNames = array_keys($columns); $columnNames[] = 'attribute_set_id'; $columnNames[] = 'type_id'; + $columnNames[] = $linkField; $select->from( ['e' => $entityTableName], $columnNames @@ -159,6 +159,7 @@ public function write($storeId, $productId, $valueFieldSuffix = '') $cursor = $this->_connection->query($select); $row = $cursor->fetch(\Zend_Db::FETCH_ASSOC); if (!empty($row)) { + $linkFieldId = $linkField; foreach ($row as $columnName => $value) { $updateData[$columnName] = $value; } @@ -170,7 +171,7 @@ public function write($storeId, $productId, $valueFieldSuffix = '') if (!empty($updateData)) { $updateData += ['entity_id' => $productId]; if ($linkField !== $metadata->getIdentifierField()) { - $updateData += [$linkField => $productId]; + $updateData += [$linkField => $linkFieldId]; } $updateFields = []; foreach ($updateData as $key => $value) { diff --git a/app/code/Magento/Catalog/Model/Indexer/Product/Flat/Action/Row.php b/app/code/Magento/Catalog/Model/Indexer/Product/Flat/Action/Row.php index 6d0727259d9db..8ccd48f1360c0 100644 --- a/app/code/Magento/Catalog/Model/Indexer/Product/Flat/Action/Row.php +++ b/app/code/Magento/Catalog/Model/Indexer/Product/Flat/Action/Row.php @@ -95,15 +95,17 @@ public function execute($id = null) /* @var $status \Magento\Eav\Model\Entity\Attribute */ $status = $this->_productIndexerHelper->getAttribute(ProductInterface::STATUS); $statusTable = $status->getBackend()->getTable(); + $catalogProductEntityTable = $this->_productIndexerHelper->getTable('catalog_product_entity'); $statusConditions = [ - 'store_id IN(0,' . (int)$store->getId() . ')', - 'attribute_id = ' . (int)$status->getId(), - $linkField . ' = ' . (int)$id, + 's.store_id IN(0,' . (int)$store->getId() . ')', + 's.attribute_id = ' . (int)$status->getId(), + 'e.entity_id = ' . (int)$id, ]; $select = $this->_connection->select(); - $select->from($statusTable, ['value']) + $select->from(['e' => $catalogProductEntityTable], ['s.value']) ->where(implode(' AND ', $statusConditions)) - ->order('store_id DESC') + ->joinLeft(['s' => $statusTable], "e.{$linkField} = s.{$linkField}", []) + ->order('s.store_id DESC') ->limit(1); $result = $this->_connection->query($select); $status = $result->fetchColumn(0); 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 abe1b3eedbc84..e8a60d50405a5 100644 --- a/app/code/Magento/Catalog/Model/Indexer/Product/Flat/FlatTableBuilder.php +++ b/app/code/Magento/Catalog/Model/Indexer/Product/Flat/FlatTableBuilder.php @@ -352,12 +352,20 @@ protected function _updateTemporaryTableByStoreValues( } //Update not simple attributes (eg. dropdown) - if (isset($flatColumns[$attributeCode . $valueFieldSuffix])) { - $select = $this->_connection->select()->joinInner( - ['t' => $this->_productIndexerHelper->getTable('eav_attribute_option_value')], - 't.option_id = et.' . $attributeCode . ' AND t.store_id=' . $storeId, - [$attributeCode . $valueFieldSuffix => 't.value'] - ); + $columnName = $attributeCode . $valueFieldSuffix; + if (isset($flatColumns[$columnName])) { + $select = $this->_connection->select(); + $select->joinLeft( + ['t0' => $this->_productIndexerHelper->getTable('eav_attribute_option_value')], + 't0.option_id = et.' . $attributeCode . ' AND t0.store_id = 0', + [] + )->joinLeft( + ['ts' => $this->_productIndexerHelper->getTable('eav_attribute_option_value')], + 'ts.option_id = et.' . $attributeCode . ' AND ts.store_id = ' . $storeId, + [] + )->columns( + [$columnName => $this->_connection->getIfNullSql('ts.value', 't0.value')] + )->where($attributeCode . ' IS NOT NULL'); if (!empty($changedIds)) { $select->where($this->_connection->quoteInto('et.entity_id IN (?)', $changedIds)); } @@ -381,6 +389,8 @@ protected function _getTemporaryTableName($tableName) } /** + * Get MetadataPool + * * @return \Magento\Framework\EntityManager\MetadataPool */ private function getMetadataPool() diff --git a/app/code/Magento/Catalog/Model/Indexer/Product/Flat/TableBuilder.php b/app/code/Magento/Catalog/Model/Indexer/Product/Flat/TableBuilder.php index 5f8be83872021..a32379b8c0a67 100644 --- a/app/code/Magento/Catalog/Model/Indexer/Product/Flat/TableBuilder.php +++ b/app/code/Magento/Catalog/Model/Indexer/Product/Flat/TableBuilder.php @@ -34,13 +34,6 @@ class TableBuilder */ private $tableBuilderFactory; - /** - * Check whether builder was executed - * - * @var bool - */ - protected $_isExecuted = false; - /** * Constructor * @@ -70,9 +63,6 @@ public function __construct( */ public function build($storeId, $changedIds, $valueFieldSuffix) { - if ($this->_isExecuted) { - return; - } $entityTableName = $this->_productIndexerHelper->getTable('catalog_product_entity'); $attributes = $this->_productIndexerHelper->getAttributes(); $eavAttributes = $this->_productIndexerHelper->getTablesStructure($attributes); @@ -117,7 +107,6 @@ public function build($storeId, $changedIds, $valueFieldSuffix) //Fill temporary tables with attributes grouped by it type $this->_fillTemporaryTable($tableName, $columns, $changedIds, $valueFieldSuffix, $storeId); } - $this->_isExecuted = true; } /** diff --git a/app/code/Magento/Catalog/Model/Product.php b/app/code/Magento/Catalog/Model/Product.php index 8a9233f176c61..831644a553b4b 100644 --- a/app/code/Magento/Catalog/Model/Product.php +++ b/app/code/Magento/Catalog/Model/Product.php @@ -714,7 +714,7 @@ public function getIdBySku($sku) public function getCategoryId() { $category = $this->_registry->registry('current_category'); - if ($category) { + if ($category && in_array($category->getId(), $this->getCategoryIds())) { return $category->getId(); } return false; diff --git a/app/code/Magento/Catalog/Model/Product/Gallery/CreateHandler.php b/app/code/Magento/Catalog/Model/Product/Gallery/CreateHandler.php index 1a3d03bf2c353..8b9703b6623ad 100644 --- a/app/code/Magento/Catalog/Model/Product/Gallery/CreateHandler.php +++ b/app/code/Magento/Catalog/Model/Product/Gallery/CreateHandler.php @@ -308,7 +308,7 @@ protected function duplicate($product) $this->resourceModel->duplicate( $this->getAttribute()->getAttributeId(), - isset($mediaGalleryData['duplicate']) ? $mediaGalleryData['duplicate'] : [], + $mediaGalleryData['duplicate'] ?? [], $product->getOriginalLinkId(), $product->getData($this->metadata->getLinkField()) ); diff --git a/app/code/Magento/Catalog/Model/Product/ProductList/ToolbarMemorizer.php b/app/code/Magento/Catalog/Model/Product/ProductList/ToolbarMemorizer.php new file mode 100644 index 0000000000000..46a73e104b87f --- /dev/null +++ b/app/code/Magento/Catalog/Model/Product/ProductList/ToolbarMemorizer.php @@ -0,0 +1,183 @@ +toolbarModel = $toolbarModel; + $this->catalogSession = $catalogSession; + $this->scopeConfig = $scopeConfig; + } + + /** + * Get sort order. + * + * @return string|bool|null + */ + public function getOrder() + { + if ($this->order === null) { + $this->order = $this->toolbarModel->getOrder() ?? + ($this->isMemorizingAllowed() ? $this->catalogSession->getData(Toolbar::ORDER_PARAM_NAME) : null); + } + + return $this->order; + } + + /** + * Get sort direction. + * + * @return string|bool|null + */ + public function getDirection() + { + if ($this->direction === null) { + $this->direction = $this->toolbarModel->getDirection() ?? + ($this->isMemorizingAllowed() ? $this->catalogSession->getData(Toolbar::DIRECTION_PARAM_NAME) : null); + } + + return $this->direction; + } + + /** + * Get sort mode. + * + * @return string|bool|null + */ + public function getMode() + { + if ($this->mode === null) { + $this->mode = $this->toolbarModel->getMode() ?? + ($this->isMemorizingAllowed() ? $this->catalogSession->getData(Toolbar::MODE_PARAM_NAME) : null); + } + + return $this->mode; + } + + /** + * Get products per page limit. + * + * @return string|bool|null + */ + public function getLimit() + { + if ($this->limit === null) { + $this->limit = $this->toolbarModel->getLimit() ?? + ($this->isMemorizingAllowed() ? $this->catalogSession->getData(Toolbar::LIMIT_PARAM_NAME) : null); + } + + return $this->limit; + } + + /** + * Method to save all catalog parameters in catalog session. + * + * @return void + */ + public function memorizeParams() + { + if (!$this->catalogSession->getParamsMemorizeDisabled() && $this->isMemorizingAllowed()) { + $this->memorizeParam(Toolbar::ORDER_PARAM_NAME, $this->getOrder()) + ->memorizeParam(Toolbar::DIRECTION_PARAM_NAME, $this->getDirection()) + ->memorizeParam(Toolbar::MODE_PARAM_NAME, $this->getMode()) + ->memorizeParam(Toolbar::LIMIT_PARAM_NAME, $this->getLimit()); + } + } + + /** + * Check configuration for enabled/disabled toolbar memorizing. + * + * @return bool + */ + public function isMemorizingAllowed(): bool + { + if ($this->isMemorizingAllowed === null) { + $this->isMemorizingAllowed = $this->scopeConfig->isSetFlag(self::XML_PATH_CATALOG_REMEMBER_PAGINATION); + } + + return $this->isMemorizingAllowed; + } + + /** + * Memorize parameter value for session. + * + * @param string $param parameter name + * @param mixed $value parameter value + * @return ToolbarMemorizer + */ + private function memorizeParam(string $param, $value): ToolbarMemorizer + { + if ($value && $this->catalogSession->getData($param) != $value) { + $this->catalogSession->setData($param, $value); + } + + return $this; + } +} diff --git a/app/code/Magento/Catalog/Model/ResourceModel/Category.php b/app/code/Magento/Catalog/Model/ResourceModel/Category.php index 56b8a19d14255..7efcaed43cab2 100644 --- a/app/code/Magento/Catalog/Model/ResourceModel/Category.php +++ b/app/code/Magento/Catalog/Model/ResourceModel/Category.php @@ -484,8 +484,20 @@ public function getProductsPosition($category) $this->getCategoryProductTable(), ['product_id', 'position'] )->where( - 'category_id = :category_id' + "{$this->getTable('catalog_category_product')}.category_id = ?", + $category->getId() ); + $websiteId = $category->getStore()->getWebsiteId(); + if ($websiteId) { + $select->join( + ['product_website' => $this->getTable('catalog_product_website')], + "product_website.product_id = {$this->getTable('catalog_category_product')}.product_id", + [] + )->where( + 'product_website.website_id = ?', + $websiteId + ); + } $bind = ['category_id' => (int)$category->getId()]; return $this->getConnection()->fetchPairs($select, $bind); diff --git a/app/code/Magento/Catalog/Model/ResourceModel/Category/Flat.php b/app/code/Magento/Catalog/Model/ResourceModel/Category/Flat.php index 9db2c8248ce52..05950531e2178 100644 --- a/app/code/Magento/Catalog/Model/ResourceModel/Category/Flat.php +++ b/app/code/Magento/Catalog/Model/ResourceModel/Category/Flat.php @@ -699,8 +699,20 @@ public function getProductsPosition($category) $this->getTable('catalog_category_product'), ['product_id', 'position'] )->where( - 'category_id = :category_id' + "{$this->getTable('catalog_category_product')}.category_id = ?", + $category->getId() ); + $websiteId = $category->getStore()->getWebsiteId(); + if ($websiteId) { + $select->join( + ['product_website' => $this->getTable('catalog_product_website')], + "product_website.product_id = {$this->getTable('catalog_category_product')}.product_id", + [] + )->where( + 'product_website.website_id = ?', + $websiteId + ); + } $bind = ['category_id' => (int)$category->getId()]; return $this->getConnection()->fetchPairs($select, $bind); diff --git a/app/code/Magento/Catalog/Model/ResourceModel/Category/RedundantCategoryImageChecker.php b/app/code/Magento/Catalog/Model/ResourceModel/Category/RedundantCategoryImageChecker.php new file mode 100644 index 0000000000000..b683bcd803bd3 --- /dev/null +++ b/app/code/Magento/Catalog/Model/ResourceModel/Category/RedundantCategoryImageChecker.php @@ -0,0 +1,52 @@ +categoryList = $categoryList; + $this->searchCriteriaBuilder = $searchCriteriaBuilder; + } + + /** + * Checks if Image is currently used in any category as Category Image. + * + * Returns true if not. + * + * @param string $imageName + * @return bool + */ + public function execute(string $imageName): bool + { + /** @var SearchCriteriaBuilder $searchCriteriaBuilder */ + $searchCriteria = $this->searchCriteriaBuilder->addFilter('image', $imageName)->create(); + $categories = $this->categoryList->getList($searchCriteria)->getItems(); + + return empty($categories); + } +} diff --git a/app/code/Magento/Catalog/Model/ResourceModel/Product/Gallery.php b/app/code/Magento/Catalog/Model/ResourceModel/Product/Gallery.php index b68c43e40ff2f..9a7af68948a21 100644 --- a/app/code/Magento/Catalog/Model/ResourceModel/Product/Gallery.php +++ b/app/code/Magento/Catalog/Model/ResourceModel/Product/Gallery.php @@ -3,6 +3,7 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ + namespace Magento\Catalog\Model\ResourceModel\Product; use Magento\Store\Model\Store; @@ -141,7 +142,7 @@ public function loadProductGalleryByAttributeId($product, $attributeId) */ protected function createBaseLoadSelect($entityId, $storeId, $attributeId) { - $select = $this->createBatchBaseSelect($storeId, $attributeId); + $select = $this->createBatchBaseSelect($storeId, $attributeId); $select = $select->where( 'entity.' . $this->metadata->getLinkField() . ' = ?', @@ -362,9 +363,9 @@ public function deleteGalleryValueInStore($valueId, $entityId, $storeId) $conditions = implode( ' AND ', [ - $this->getConnection()->quoteInto('value_id = ?', (int) $valueId), - $this->getConnection()->quoteInto($this->metadata->getLinkField() . ' = ?', (int) $entityId), - $this->getConnection()->quoteInto('store_id = ?', (int) $storeId) + $this->getConnection()->quoteInto('value_id = ?', (int)$valueId), + $this->getConnection()->quoteInto($this->metadata->getLinkField() . ' = ?', (int)$entityId), + $this->getConnection()->quoteInto('store_id = ?', (int)$storeId) ] ); @@ -392,7 +393,7 @@ public function duplicate($attributeId, $newFiles, $originalProductId, $newProdu $select = $this->getConnection()->select()->from( [$this->getMainTableAlias() => $this->getMainTable()], - ['value_id', 'value'] + ['value_id', 'value', 'media_type', 'disabled'] )->joinInner( ['entity' => $this->getTable(self::GALLERY_VALUE_TO_ENTITY_TABLE)], $this->getMainTableAlias() . '.value_id = entity.value_id', @@ -409,16 +410,16 @@ public function duplicate($attributeId, $newFiles, $originalProductId, $newProdu // Duplicate main entries of gallery foreach ($this->getConnection()->fetchAll($select) as $row) { - $data = [ - 'attribute_id' => $attributeId, - 'value' => isset($newFiles[$row['value_id']]) ? $newFiles[$row['value_id']] : $row['value'], - ]; + $data = $row; + $data['attribute_id'] = $attributeId; + $data['value'] = $newFiles[$row['value_id']] ?? $row['value']; + unset($data['value_id']); $valueIdMap[$row['value_id']] = $this->insertGallery($data); $this->bindValueToEntity($valueIdMap[$row['value_id']], $newProductId); } - if (count($valueIdMap) == 0) { + if (count($valueIdMap) === 0) { return []; } diff --git a/app/code/Magento/Catalog/Model/ResourceModel/Product/Image.php b/app/code/Magento/Catalog/Model/ResourceModel/Product/Image.php index 5f83f9826abb5..77f67480619e0 100644 --- a/app/code/Magento/Catalog/Model/ResourceModel/Product/Image.php +++ b/app/code/Magento/Catalog/Model/ResourceModel/Product/Image.php @@ -13,7 +13,7 @@ use Magento\Framework\App\ResourceConnection; /** - * Class for fast retrieval of all product images + * Class for retrieval of all product images */ class Image { @@ -76,15 +76,24 @@ public function getAllProductImages(): \Generator /** * Get the number of unique pictures of products + * * @return int */ public function getCountAllProductImages(): int { - $select = $this->getVisibleImagesSelect()->reset('columns')->columns('count(*)'); + $select = $this->getVisibleImagesSelect() + ->reset('columns') + ->reset('distinct') + ->columns( + new \Zend_Db_Expr('count(distinct value)') + ); + return (int) $this->connection->fetchOne($select); } /** + * Return Select to fetch all products images + * * @return Select */ private function getVisibleImagesSelect(): Select diff --git a/app/code/Magento/Catalog/Plugin/Framework/App/Action/ContextPlugin.php b/app/code/Magento/Catalog/Plugin/Framework/App/Action/ContextPlugin.php new file mode 100644 index 0000000000000..544319e739de5 --- /dev/null +++ b/app/code/Magento/Catalog/Plugin/Framework/App/Action/ContextPlugin.php @@ -0,0 +1,74 @@ +toolbarMemorizer = $toolbarMemorizer; + $this->catalogSession = $catalogSession; + $this->httpContext = $httpContext; + } + + /** + * Update http context with catalog sensitive information. + * + * @return void + */ + public function beforeDispatch() + { + if ($this->toolbarMemorizer->isMemorizingAllowed()) { + $params = [ + ToolbarModel::ORDER_PARAM_NAME, + ToolbarModel::DIRECTION_PARAM_NAME, + ToolbarModel::MODE_PARAM_NAME, + ToolbarModel::LIMIT_PARAM_NAME, + ]; + + foreach ($params as $param) { + $paramValue = $this->catalogSession->getData($param); + if ($paramValue) { + $this->httpContext->setValue($param, $paramValue, false); + } + } + } + } +} diff --git a/app/code/Magento/Catalog/Plugin/Model/ResourceModel/Category/RemoveRedundantImagePlugin.php b/app/code/Magento/Catalog/Plugin/Model/ResourceModel/Category/RemoveRedundantImagePlugin.php new file mode 100644 index 0000000000000..59f1051b8ed56 --- /dev/null +++ b/app/code/Magento/Catalog/Plugin/Model/ResourceModel/Category/RemoveRedundantImagePlugin.php @@ -0,0 +1,77 @@ +filesystem = $filesystem; + $this->imageUploader = $imageUploader; + $this->redundantCategoryImageChecker = $redundantCategoryImageChecker; + } + + /** + * Removes Image file if it is not used anymore. + * + * @param CategoryResource $subject + * @param CategoryResource $result + * @param AbstractModel $category + * @return CategoryResource + * + * @throws \Magento\Framework\Exception\FileSystemException + * @SuppressWarnings(PHPMD.UnusedFormalParameter) + */ + public function afterSave( + CategoryResource $subject, + CategoryResource $result, + AbstractModel $category + ): CategoryResource { + $originalImage = $category->getOrigData('image'); + if (null !== $originalImage + && $originalImage !== $category->getImage() + && $this->redundantCategoryImageChecker->execute($originalImage) + ) { + $basePath = $this->imageUploader->getBasePath(); + $baseImagePath = $this->imageUploader->getFilePath($basePath, $originalImage); + /** @var \Magento\Framework\Filesystem\Directory\WriteInterface $mediaDirectory */ + $mediaDirectory = $this->filesystem->getDirectoryWrite(DirectoryList::MEDIA); + $mediaDirectory->delete($baseImagePath); + } + + return $result; + } +} diff --git a/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AddProductToCartActionGroup.xml b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AddProductToCartActionGroup.xml index 7dafeff34a2ea..02dadf0d00337 100644 --- a/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AddProductToCartActionGroup.xml +++ b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AddProductToCartActionGroup.xml @@ -7,7 +7,7 @@ --> + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> @@ -15,7 +15,8 @@ - . + + @@ -26,6 +27,15 @@ + + + + + + + + + diff --git a/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AdminCategoryActionGroup.xml b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AdminCategoryActionGroup.xml index 127b69e5c3dc4..6d43031e2f8aa 100644 --- a/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AdminCategoryActionGroup.xml +++ b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AdminCategoryActionGroup.xml @@ -87,4 +87,39 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AdminProductActionGroup.xml b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AdminProductActionGroup.xml index f513aaf1e5654..d8e7f7ea710d6 100644 --- a/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AdminProductActionGroup.xml +++ b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AdminProductActionGroup.xml @@ -7,7 +7,7 @@ --> + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> @@ -97,11 +97,10 @@ - + - @@ -149,7 +148,6 @@ - @@ -169,6 +167,21 @@ + + + + + + + + + + + {{amount}} + $grabProductTierPriceInput + + + @@ -188,4 +201,29 @@ + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AdminProductAttributeActionGroup.xml b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AdminProductAttributeActionGroup.xml index daa4cfee37691..1c6f115c2cfce 100644 --- a/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AdminProductAttributeActionGroup.xml +++ b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AdminProductAttributeActionGroup.xml @@ -36,7 +36,12 @@ - + + + + + + diff --git a/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AdminProductAttributeSetActionGroup.xml b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AdminProductAttributeSetActionGroup.xml index 17f27056c555e..9817e24bed963 100644 --- a/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AdminProductAttributeSetActionGroup.xml +++ b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AdminProductAttributeSetActionGroup.xml @@ -49,6 +49,7 @@ + diff --git a/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AdminProductGridActionGroup.xml b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AdminProductGridActionGroup.xml index 3c4e83aabd047..a6213c459396a 100644 --- a/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AdminProductGridActionGroup.xml +++ b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AdminProductGridActionGroup.xml @@ -7,7 +7,7 @@ --> + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> @@ -33,6 +33,7 @@ + @@ -48,10 +49,20 @@ - + + + + + + + + + + + @@ -71,7 +82,14 @@ + + + + + + + diff --git a/app/code/Magento/Catalog/Test/Mftf/ActionGroup/SearchForProductOnBackendActionGroup.xml b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/SearchForProductOnBackendActionGroup.xml index ebeee87b1c89e..6cb939a7af410 100644 --- a/app/code/Magento/Catalog/Test/Mftf/ActionGroup/SearchForProductOnBackendActionGroup.xml +++ b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/SearchForProductOnBackendActionGroup.xml @@ -7,7 +7,7 @@ --> + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> @@ -19,4 +19,11 @@ + + + + + + + diff --git a/app/code/Magento/Catalog/Test/Mftf/ActionGroup/StorefrontAddToCartCustomOptionsProductPageActionGroup.xml b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/StorefrontAddToCartCustomOptionsProductPageActionGroup.xml index 4938b6a9592f1..91afd2eb5d4c7 100644 --- a/app/code/Magento/Catalog/Test/Mftf/ActionGroup/StorefrontAddToCartCustomOptionsProductPageActionGroup.xml +++ b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/StorefrontAddToCartCustomOptionsProductPageActionGroup.xml @@ -13,7 +13,6 @@ - diff --git a/app/code/Magento/Catalog/Test/Mftf/ActionGroup/StorefrontCategoryActionGroup.xml b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/StorefrontCategoryActionGroup.xml index 8f0df2fc899a9..2551bd61580ed 100644 --- a/app/code/Magento/Catalog/Test/Mftf/ActionGroup/StorefrontCategoryActionGroup.xml +++ b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/StorefrontCategoryActionGroup.xml @@ -47,4 +47,34 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/code/Magento/Catalog/Test/Mftf/Data/CatalogStorefrontConfigData.xml b/app/code/Magento/Catalog/Test/Mftf/Data/CatalogStorefrontConfigData.xml new file mode 100644 index 0000000000000..850190f0fd24c --- /dev/null +++ b/app/code/Magento/Catalog/Test/Mftf/Data/CatalogStorefrontConfigData.xml @@ -0,0 +1,49 @@ + + + + + + GridPerPageValues + RememberCategoryPagination + + + + 9,12,20,24 + + + + 1 + + + + 9,15,30 + + + + 1 + + + + DefaultCatalogStorefrontFlagZero + DefaultGridPerPageValues + DefaultRememberCategoryPagination + + + + 0 + + + + 0 + + + + 0 + + diff --git a/app/code/Magento/Catalog/Test/Mftf/Data/CategoryData.xml b/app/code/Magento/Catalog/Test/Mftf/Data/CategoryData.xml index 8d7c182d648c2..e798cda2daa5a 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Data/CategoryData.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Data/CategoryData.xml @@ -39,4 +39,12 @@ + + Gear + gear + + + Bags + bags + diff --git a/app/code/Magento/Catalog/Test/Mftf/Data/ProductAttributeOptionData.xml b/app/code/Magento/Catalog/Test/Mftf/Data/ProductAttributeOptionData.xml index 25512c0f266e9..ace2f2e6a02ab 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Data/ProductAttributeOptionData.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Data/ProductAttributeOptionData.xml @@ -7,7 +7,7 @@ --> + xsi:noNamespaceSchemaLocation="urn:magento:mftf:DataGenerator/etc/dataProfileSchema.xsd"> option1 @@ -59,4 +59,20 @@ Option6Store0 Option6Store1 + + + Green + false + 3 + Option7Store0 + Option8Store1 + + + + Red + false + 3 + Option9Store0 + Option10Store1 + diff --git a/app/code/Magento/Catalog/Test/Mftf/Data/ProductConfigurableAttributeData.xml b/app/code/Magento/Catalog/Test/Mftf/Data/ProductConfigurableAttributeData.xml index 4deebbe09af34..0446a9db14f1a 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Data/ProductConfigurableAttributeData.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Data/ProductConfigurableAttributeData.xml @@ -7,10 +7,11 @@ --> + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/dataProfileSchema.xsd"> Color 1 + Dropdown White diff --git a/app/code/Magento/Catalog/Test/Mftf/Data/ProductData.xml b/app/code/Magento/Catalog/Test/Mftf/Data/ProductData.xml index a6f2e60e843be..39f4f0f2d997d 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Data/ProductData.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Data/ProductData.xml @@ -113,6 +113,17 @@ EavStockItem CustomAttributeProductAttribute + + SimpleTwo + simple + 4 + SimpleProduct + 1.23 + 4 + 1 + EavStockItem + CustomAttributeProductUrlKey + SimpleOne simple @@ -124,6 +135,9 @@ EavStockItem CustomAttributeProductAttribute + + 1 + Image1 1.00 diff --git a/app/code/Magento/Catalog/Test/Mftf/Data/StoreLabelData.xml b/app/code/Magento/Catalog/Test/Mftf/Data/StoreLabelData.xml index a703e56beda01..37489ac8143b9 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Data/StoreLabelData.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Data/StoreLabelData.xml @@ -7,7 +7,7 @@ --> + xsi:noNamespaceSchemaLocation="urn:magento:mftf:DataGenerator/etc/dataProfileSchema.xsd"> 0 option1 @@ -56,4 +56,20 @@ 1 option6 + + 0 + Green + + + 1 + Green + + + 0 + Red + + + 1 + Red + diff --git a/app/code/Magento/Catalog/Test/Mftf/Metadata/catalog_configuration-meta.xml b/app/code/Magento/Catalog/Test/Mftf/Metadata/catalog_configuration-meta.xml new file mode 100644 index 0000000000000..4de036565eee3 --- /dev/null +++ b/app/code/Magento/Catalog/Test/Mftf/Metadata/catalog_configuration-meta.xml @@ -0,0 +1,118 @@ + + + + + + + + + string + + + string + + + string + + + string + + + string + + + string + + + integer + + + integer + + + integer + + + integer + + + string + + + integer + + + + + + + + + + + + + integer + + + + + integer + + + + + integer + + + + + integer + + + + + integer + + + + + integer + + + + + integer + + + + + integer + + + + + integer + + + + + integer + + + + integer + + + integer + + + + + + diff --git a/app/code/Magento/Catalog/Test/Mftf/Page/AdminCategoryEditPage.xml b/app/code/Magento/Catalog/Test/Mftf/Page/AdminCategoryEditPage.xml index 68c432a136ac9..c031578e2a208 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Page/AdminCategoryEditPage.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Page/AdminCategoryEditPage.xml @@ -16,5 +16,7 @@
+
+
diff --git a/app/code/Magento/Catalog/Test/Mftf/Page/AdminCategoryPage.xml b/app/code/Magento/Catalog/Test/Mftf/Page/AdminCategoryPage.xml index 45485509f5e3e..54e9194ca450d 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Page/AdminCategoryPage.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Page/AdminCategoryPage.xml @@ -7,8 +7,8 @@ --> - + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Page/etc/PageObject.xsd"> +
diff --git a/app/code/Magento/Catalog/Test/Mftf/Page/AdminProductIndexPage.xml b/app/code/Magento/Catalog/Test/Mftf/Page/AdminProductIndexPage.xml index d77a041baf662..03915ba68b642 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Page/AdminProductIndexPage.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Page/AdminProductIndexPage.xml @@ -12,6 +12,7 @@
+
diff --git a/app/code/Magento/Catalog/Test/Mftf/Page/StorefrontCategoryPage.xml b/app/code/Magento/Catalog/Test/Mftf/Page/StorefrontCategoryPage.xml index b32c3ded033f9..0ada59c623451 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Page/StorefrontCategoryPage.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Page/StorefrontCategoryPage.xml @@ -11,5 +11,6 @@
+
diff --git a/app/code/Magento/Catalog/Test/Mftf/Section/AdminCategoryBasicFieldSection.xml b/app/code/Magento/Catalog/Test/Mftf/Section/AdminCategoryBasicFieldSection.xml index 555b3c2a6e00e..a32ed228c8570 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Section/AdminCategoryBasicFieldSection.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Section/AdminCategoryBasicFieldSection.xml @@ -7,7 +7,7 @@ --> + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Page/etc/SectionObject.xsd">
@@ -17,6 +17,9 @@ + + +
diff --git a/app/code/Magento/Catalog/Test/Mftf/Section/AdminCategoryContentSection.xml b/app/code/Magento/Catalog/Test/Mftf/Section/AdminCategoryContentSection.xml new file mode 100644 index 0000000000000..faa320cd114de --- /dev/null +++ b/app/code/Magento/Catalog/Test/Mftf/Section/AdminCategoryContentSection.xml @@ -0,0 +1,15 @@ + + + + +
+ + +
+
diff --git a/app/code/Magento/Catalog/Test/Mftf/Section/AdminCategoryDisplaySettingsSection.xml b/app/code/Magento/Catalog/Test/Mftf/Section/AdminCategoryDisplaySettingsSection.xml new file mode 100644 index 0000000000000..d545feca3e711 --- /dev/null +++ b/app/code/Magento/Catalog/Test/Mftf/Section/AdminCategoryDisplaySettingsSection.xml @@ -0,0 +1,15 @@ + + + + +
+ + +
+
diff --git a/app/code/Magento/Catalog/Test/Mftf/Section/AdminProductFormAdvancedPricingSection.xml b/app/code/Magento/Catalog/Test/Mftf/Section/AdminProductFormAdvancedPricingSection.xml index 0f1fe8abeb3b5..ef66a41e27d06 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Section/AdminProductFormAdvancedPricingSection.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Section/AdminProductFormAdvancedPricingSection.xml @@ -18,6 +18,7 @@ +
diff --git a/app/code/Magento/Catalog/Test/Mftf/Section/AdminProductFormSection.xml b/app/code/Magento/Catalog/Test/Mftf/Section/AdminProductFormSection.xml index 6e86420ff9de5..7a97a75556769 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Section/AdminProductFormSection.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Section/AdminProductFormSection.xml @@ -7,7 +7,7 @@ --> + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Page/etc/SectionObject.xsd">
@@ -21,6 +21,7 @@ + @@ -37,6 +38,8 @@ + +
@@ -185,5 +188,6 @@
+
diff --git a/app/code/Magento/Catalog/Test/Mftf/Section/AdminProductGridSection.xml b/app/code/Magento/Catalog/Test/Mftf/Section/AdminProductGridSection.xml index 6ffecc341123d..e6d9cae3e9442 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Section/AdminProductGridSection.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Section/AdminProductGridSection.xml @@ -7,8 +7,9 @@ --> + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Page/etc/SectionObject.xsd">
+ diff --git a/app/code/Magento/Catalog/Test/Mftf/Section/AdminProductGridTableHeaderSection.xml b/app/code/Magento/Catalog/Test/Mftf/Section/AdminProductGridTableHeaderSection.xml new file mode 100644 index 0000000000000..7341a6ded7a09 --- /dev/null +++ b/app/code/Magento/Catalog/Test/Mftf/Section/AdminProductGridTableHeaderSection.xml @@ -0,0 +1,14 @@ + + + + +
+ +
+
diff --git a/app/code/Magento/Catalog/Test/Mftf/Section/StorefrontCategoryPagerSection.xml b/app/code/Magento/Catalog/Test/Mftf/Section/StorefrontCategoryPagerSection.xml new file mode 100644 index 0000000000000..64470ec1f40e4 --- /dev/null +++ b/app/code/Magento/Catalog/Test/Mftf/Section/StorefrontCategoryPagerSection.xml @@ -0,0 +1,17 @@ + + + + +
+ + + + +
+
diff --git a/app/code/Magento/Catalog/Test/Mftf/Section/StorefrontMessagesSection.xml b/app/code/Magento/Catalog/Test/Mftf/Section/StorefrontMessagesSection.xml index f1456ac8ee387..1b97d0c0795a4 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Section/StorefrontMessagesSection.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Section/StorefrontMessagesSection.xml @@ -7,10 +7,11 @@ --> + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Page/etc/SectionObject.xsd">
+
diff --git a/app/code/Magento/Catalog/Test/Mftf/Section/StorefrontProductInfoMainSection.xml b/app/code/Magento/Catalog/Test/Mftf/Section/StorefrontProductInfoMainSection.xml index 65fe9b06be66c..4eb11727849e9 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Section/StorefrontProductInfoMainSection.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Section/StorefrontProductInfoMainSection.xml @@ -7,12 +7,13 @@ --> + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Page/etc/SectionObject.xsd">
+ diff --git a/app/code/Magento/Catalog/Test/Mftf/Section/StorefrontProductPageSection.xml b/app/code/Magento/Catalog/Test/Mftf/Section/StorefrontProductPageSection.xml index c960f8432e64d..61eabb6cb34fd 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Section/StorefrontProductPageSection.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Section/StorefrontProductPageSection.xml @@ -7,10 +7,10 @@ --> + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Page/etc/SectionObject.xsd">
- - + + diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminFilteringCategoryProductsUsingScopeSelectorTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminFilteringCategoryProductsUsingScopeSelectorTest.xml index ba0eb20ce7733..fb64e66f8aabd 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminFilteringCategoryProductsUsingScopeSelectorTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminFilteringCategoryProductsUsingScopeSelectorTest.xml @@ -7,7 +7,7 @@ --> + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> @@ -19,19 +19,19 @@ - - - - + + + + - - - - + + + + - - - + + + @@ -74,7 +74,7 @@ - + @@ -89,22 +89,27 @@ - + - - - - + + + + + + + + + @@ -156,7 +161,7 @@ - diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminRequiredFieldsHaveRequiredFieldIndicatorTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminRequiredFieldsHaveRequiredFieldIndicatorTest.xml new file mode 100644 index 0000000000000..532ef76121857 --- /dev/null +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminRequiredFieldsHaveRequiredFieldIndicatorTest.xml @@ -0,0 +1,53 @@ + + + + + + + + + <description value="Verify that Required fields should have the required indicator icon next to the field name"/> + <severity value="MAJOR"/> + <testCaseId value="MAGETWO-97037"/> + <group value="catalog"/> + <group value="cms"/> + </annotations> + <before> + <actionGroup ref="LoginAsAdmin" stepKey="loginAsAdmin"/> + </before> + <after> + <actionGroup ref="logout" stepKey="logoutFromAdmin"/> + </after> + + <amOnPage url="{{AdminCategoryPage.url}}" stepKey="navigateToCategoryPage"/> + <click selector="{{AdminCategorySidebarActionSection.AddSubcategoryButton}}" stepKey="clickOnAddSubCategory"/> + + <!-- Verify that the Category Name field has the required field name indicator --> + <executeJS function="{{AdminCategoryBasicFieldSection.requiredFieldIndicator}}" stepKey="getRequiredFieldIndicator"/> + <assertEquals expected='"*"' expectedType="string" actualType="variable" actual="getRequiredFieldIndicator" stepKey="assertRequiredFieldIndicator"/> + + <executeJS function="{{AdminCategoryBasicFieldSection.requiredFieldIndicatorColor}}" stepKey="getRequiredFieldIndicatorColor"/> + <assertEquals expected="rgb(226, 38, 38)" expectedType="string" actualType="variable" actual="getRequiredFieldIndicatorColor" stepKey="assertRequiredFieldIndicator1"/> + + <amOnPage url="{{AdminProductIndexPage.url}}" stepKey="navigateToProductIndexPage"/> + <click selector="{{AdminProductGridActionSection.addProductToggle}}" stepKey="addProductDropdown"/> + <click selector="{{AdminProductGridActionSection.addSimpleProduct}}" stepKey="addSimpleProduct"/> + + <!-- Verify that the Product Name and Sku fields have the required field name indicator --> + <executeJS function="{{AdminProductFormSection.requiredNameIndicator}}" stepKey="productNameRequiredFieldIndicator"/> + <assertEquals expected='"*"' expectedType="string" actualType="variable" actual="productNameRequiredFieldIndicator" stepKey="assertRequiredFieldIndicator2"/> + <executeJS function="{{AdminProductFormSection.requiredSkuIndicator}}" stepKey="productSkuRequiredFieldIndicator"/> + <assertEquals expected='"*"' expectedType="string" actualType="variable" actual="productSkuRequiredFieldIndicator" stepKey="assertRequiredFieldIndicator3"/> + + <!-- Verify that the CMS page have the required field name indicator next to Page Title --> + <amOnPage url="{{CmsPagesPage.url}}" stepKey="amOnPagePagesGrid"/> + <click selector="{{CmsPagesPageActionsSection.addNewPage}}" stepKey="clickAddNewPage"/> + <executeJS function="{{CmsNewPagePageBasicFieldsSection.requiredFieldIndicator}}" stepKey="pageTitleRequiredFieldIndicator"/> + <assertEquals expected='"*"' expectedType="string" actualType="variable" actual="pageTitleRequiredFieldIndicator" stepKey="assertRequiredFieldIndicator4"/> + </test> +</tests> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminValidateApplyingTierPriceWithEmptyDiscountValueTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminValidateApplyingTierPriceWithEmptyDiscountValueTest.xml new file mode 100644 index 0000000000000..8555615cc8781 --- /dev/null +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminValidateApplyingTierPriceWithEmptyDiscountValueTest.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="AdminValidateApplyingTierPriceWithEmptyDiscountValueTest"> + <annotations> + <features value="Apply tier price"/> + <title value="Apply Tier Price with empty discount value"/> + <description value="Validate applying tier price to product"/> + <stories value="Apply Tier Price with empty discount value" /> + <severity value="CRITICAL"/> + <testCaseId value="MAGETWO-96484"/> + <group value="product"/> + </annotations> + <before> + <createData entity="_defaultCategory" stepKey="createCategory"/> + <createData entity="SimpleProduct" stepKey="createSimpleProduct"> + <requiredEntity createDataKey="createCategory"/> + </createData> + </before> + <after> + <deleteData createDataKey="createSimpleProduct" stepKey="deleteSimpleProduct"/> + <deleteData createDataKey="createCategory" stepKey="deleteCategory"/> + <amOnPage url="{{AdminProductIndexPage.url}}" stepKey="navigateToProductIndex"/> + <actionGroup ref="AdminResetProductGridToDefaultViewActionGroup" stepKey="resetGridToDefaultKeywordSearch"/> + <actionGroup ref="logout" stepKey="logoutFromAdmin"/> + </after> + + <actionGroup ref="LoginAsAdmin" stepKey="loginAsAdmin"/> + <actionGroup ref="SearchForProductOnBackendActionGroup" stepKey="searchForSimpleProduct"> + <argument name="product" value="$$createSimpleProduct$$"/> + </actionGroup> + <actionGroup ref="OpenEditProductOnBackendActionGroup" stepKey="openEditProductPage"> + <argument name="product" value="$$createSimpleProduct$$"/> + </actionGroup> + <scrollToTopOfPage stepKey="scrollToTopOfPage"/> + <click selector="{{AdminProductFormSection.advancedPricingLink}}" stepKey="clickOnAdvancedPricingButton"/> + <waitForElement selector="{{AdminProductFormAdvancedPricingSection.customerGroupPriceAddButton}}" stepKey="waitForCustomerGroupPriceAddButton"/> + <click selector="{{AdminProductFormAdvancedPricingSection.customerGroupPriceAddButton}}" stepKey="addCustomerGroupPrice"/> + <fillField selector="{{AdminProductFormAdvancedPricingSection.productTierPriceQtyInput('0')}}" userInput="1" stepKey="fillProductTierPriceQtyInput"/> + <selectOption selector="{{AdminProductFormAdvancedPricingSection.productTierPriceValueTypeSelect('0')}}" userInput="Discount" stepKey="selectProductTierPriceValueType"/> + <clearField selector="{{AdminProductFormAdvancedPricingSection.productTierPricePercentageValuePriceInput('0')}}" stepKey="clearPercentageValueField"/> + <click selector="{{AdminProductFormAdvancedPricingSection.doneButton}}" stepKey="clickDoneButton"/> + <see selector="{{AdminProductFormAdvancedPricingSection.productTierPricePercentageError}}" userInput="This is a required field." stepKey="assertPercentageError"/> + <fillField selector="{{AdminProductFormAdvancedPricingSection.productTierPricePercentageValuePriceInput('0')}}" userInput="10" stepKey="setPercentageValue"/> + <click selector="{{AdminProductFormAdvancedPricingSection.doneButton}}" stepKey="clickDoneButton1"/> + <dontSee selector="{{AdminProductFormAdvancedPricingSection.productTierPricePercentageError}}" userInput="This is a required field." stepKey="assertNoPercentageError"/> + <actionGroup ref="SaveProductOnProductPageOnAdmin" stepKey="saveProduct"/> + </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 4058112a2d76c..a1d9b4fb7b9a2 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/CheckTierPricingOfProductsTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/CheckTierPricingOfProductsTest.xml @@ -113,6 +113,9 @@ <argument name="website" value="SecondWebsite"/> </actionGroup> + <!--Flush cache--> + <magentoCLI command="cache:flush" stepKey="cleanCache"/> + <amOnPage url="{{AdminProductIndexPage.url}}" stepKey="navigateToProductIndex"/> <waitForPageLoad stepKey="waitForProductsIndexPageToLoad"/> <actionGroup ref="resetAdminDataGridToDefaultView" stepKey="resetProductsGrid"/> @@ -155,10 +158,9 @@ <!--Create new order--> <actionGroup ref="navigateToNewOrderPageExistingCustomer" stepKey="startToCreateNewOrder"> <argument name="customer" value="$$createCustomer$$"/> + <argument name="storeView" value="SecondStoreUnique"/> </actionGroup> - <click selector="{{AdminOrderFormSelectWebsiteSection.website(SecondStoreUnique.name)}}" stepKey="selectStore"/> - <waitForPageLoad stepKey="waitForPageOpened"/> - <waitForLoadingMaskToDisappear stepKey="waitForLoadingMask"/> + <click selector="{{AdminOrderFormItemsSection.addProducts}}" stepKey="clickToAddProduct"/> <waitForPageLoad stepKey="waitForProductsOpened"/> <!--TEST CASE #1--> @@ -328,13 +330,12 @@ <deleteData createDataKey="createCustomer" stepKey="deleteCustomer"/> <createData entity="DefaultConfigCatalogPrice" stepKey="resetPriceScopeConfiguration"/> - - <actionGroup ref="AdminDeleteWebsiteActionGroup" stepKey="deleteSecondWebsite"> - <argument name="websiteName" value="{{SecondWebsite.name}}"/> - </actionGroup> <actionGroup ref="DeleteCartPriceRuleByName" stepKey="deleteCartPriceRule"> <argument name="ruleName" value="ship"/> </actionGroup> + <actionGroup ref="AdminDeleteWebsiteActionGroup" stepKey="deleteSecondWebsite"> + <argument name="websiteName" value="{{SecondWebsite.name}}"/> + </actionGroup> <actionGroup ref="logout" stepKey="logout"/> </after> </test> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/StorefrontPurchaseProductWithCustomOptionsTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/StorefrontPurchaseProductWithCustomOptionsTest.xml index f3b23206cfeb5..94cdf4e22b25c 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/StorefrontPurchaseProductWithCustomOptionsTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/StorefrontPurchaseProductWithCustomOptionsTest.xml @@ -7,7 +7,7 @@ --> <tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" - xsi:noNamespaceSchemaLocation="../../../../../../../dev/tests/acceptance/vendor/magento/magento2-functional-testing-framework/src/Magento/FunctionalTestingFramework/Test/etc/testSchema.xsd"> + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> <test name="StorefrontPurchaseProductWithCustomOptionsTest"> <annotations> <features value="Purchase a product with Custom Options of different types"/> @@ -23,6 +23,8 @@ </before> <after> <deleteData createDataKey="createCustomer" stepKey="deleteCustomer"/> + <actionGroup ref="AdminOrdersGridClearFiltersActionGroup" stepKey="clearOrderListingFilters"/> + <actionGroup ref="logout" stepKey="logoutAdmin"/> </after> <!--Create Simple Product with Custom Options--> @@ -36,10 +38,9 @@ <!-- Login Customer Storeront --> - <amOnPage url="{{StorefrontCustomerSignInPage.url}}" stepKey="amOnSignInPage"/> - <fillField userInput="$$createCustomer.email$$" selector="{{StorefrontCustomerSignInFormSection.emailField}}" stepKey="fillEmail"/> - <fillField userInput="$$createCustomer.password$$" selector="{{StorefrontCustomerSignInFormSection.passwordField}}" stepKey="fillPassword"/> - <click selector="{{StorefrontCustomerSignInFormSection.signInAccountButton}}" stepKey="clickSignInAccountButton"/> + <actionGroup ref="CustomerLoginOnStorefront" stepKey="loginCustomerOnStorefront"> + <argument name="customer" value="$$createCustomer$$"/> + </actionGroup> <!-- Checking the correctness of displayed prices for user parameters --> @@ -53,8 +54,12 @@ <seeElement selector="{{StorefrontProductInfoMainSection.productAttributeOptionsMultiselect(ProductOptionMultiSelect.title, ProductOptionValueMultiSelect1.price)}}" stepKey="checkMultiSelectProductOption"/> <seeElement selector="{{StorefrontProductInfoMainSection.productAttributeOptionsData(ProductOptionDate.title, ProductOptionDate.price)}}" stepKey="checkDataProductOption"/> - <!-- Adding items to the checkout --> + <!--Generate year--> + <generateDate date="Now" format="Y" stepKey="year"/> + <generateDate date="Now" format="y" stepKey="shortYear"/> + <!-- Adding items to the checkout --> + <fillField userInput="OptionField" selector="{{StorefrontProductInfoMainSection.productOptionFieldInput(ProductOptionField.title)}}" stepKey="fillProductOptionInputField"/> <fillField userInput="OptionArea" selector="{{StorefrontProductInfoMainSection.productOptionAreaInput(ProductOptionArea.title)}}" stepKey="fillProductOptionInputArea"/> <attachFile userInput="{{productWithOptions.file}}" selector="{{StorefrontProductInfoMainSection.addLinkFileUploadFile(ProductOptionFile.title)}}" stepKey="fillUploadFile"/> @@ -64,10 +69,10 @@ <selectOption userInput="{{ProductOptionValueMultiSelect1.price}}" selector="{{StorefrontProductInfoMainSection.productOptionSelect(ProductOptionMultiSelect.title)}}" stepKey="selectProductOptionMultiSelect"/> <selectOption userInput="01" selector="{{StorefrontProductInfoMainSection.productOptionDataMonth(ProductOptionDate.title)}}" stepKey="selectProductOptionDate"/> <selectOption userInput="01" selector="{{StorefrontProductInfoMainSection.productOptionDataDay(ProductOptionDate.title)}}" stepKey="selectProductOptionDate1"/> - <selectOption userInput="2018" selector="{{StorefrontProductInfoMainSection.productOptionDataYear(ProductOptionDate.title)}}" stepKey="selectProductOptionDate2"/> + <selectOption userInput="$year" selector="{{StorefrontProductInfoMainSection.productOptionDataYear(ProductOptionDate.title)}}" stepKey="selectProductOptionDate2"/> <selectOption userInput="01" selector="{{StorefrontProductInfoMainSection.productOptionDateAndTimeMonth(ProductOptionDateTime.title)}}" stepKey="selectProductOptionDateAndTimeMonth"/> <selectOption userInput="01" selector="{{StorefrontProductInfoMainSection.productOptionDateAndTimeDay(ProductOptionDateTime.title)}}" stepKey="selectProductOptionDateAndTimeDay"/> - <selectOption userInput="2018" selector="{{StorefrontProductInfoMainSection.productOptionDateAndTimeYear(ProductOptionDateTime.title)}}" stepKey="selectProductOptionDateAndTimeYear"/> + <selectOption userInput="$year" selector="{{StorefrontProductInfoMainSection.productOptionDateAndTimeYear(ProductOptionDateTime.title)}}" stepKey="selectProductOptionDateAndTimeYear"/> <selectOption userInput="01" selector="{{StorefrontProductInfoMainSection.productOptionDateAndTimeHour(ProductOptionDateTime.title)}}" stepKey="selectProductOptionDateAndTimeHour"/> <selectOption userInput="00" selector="{{StorefrontProductInfoMainSection.productOptionDateAndTimeMinute(ProductOptionDateTime.title)}}" stepKey="selectProductOptionDateAndTimeMinute"/> <selectOption userInput="01" selector="{{StorefrontProductInfoMainSection.productOptionTimeHour(ProductOptionTime.title)}}" stepKey="selectProductOptionTimeHour"/> @@ -101,8 +106,8 @@ <see selector="{{CheckoutPaymentSection.productOptionsActiveByProductItemName($createProduct.name$)}}" userInput="{{ProductOptionValueRadioButtons1.title}}" stepKey="seeProductOptionValueRadioButtons1Input1"/> <see selector="{{CheckoutPaymentSection.productOptionsActiveByProductItemName($createProduct.name$)}}" userInput="{{ProductOptionValueCheckbox.title}}" stepKey="seeProductOptionValueCheckboxInput1" /> <see selector="{{CheckoutPaymentSection.productOptionsActiveByProductItemName($createProduct.name$)}}" userInput="{{ProductOptionValueMultiSelect1.title}}" stepKey="seeproductAttributeOptionsMultiselect1Input1" /> - <see selector="{{CheckoutPaymentSection.productOptionsActiveByProductItemName($createProduct.name$)}}" userInput="Jan 1, 2018" stepKey="seeProductOptionDateAndTimeInput" /> - <see selector="{{CheckoutPaymentSection.productOptionsActiveByProductItemName($createProduct.name$)}}" userInput="1/1/18, 1:00 AM" stepKey="seeProductOptionDataInput" /> + <see selector="{{CheckoutPaymentSection.productOptionsActiveByProductItemName($createProduct.name$)}}" userInput="Jan 1, $year" stepKey="seeProductOptionDateAndTimeInput" /> + <see selector="{{CheckoutPaymentSection.productOptionsActiveByProductItemName($createProduct.name$)}}" userInput="1/1/$shortYear, 1:00 AM" stepKey="seeProductOptionDataInput" /> <see selector="{{CheckoutPaymentSection.productOptionsActiveByProductItemName($createProduct.name$)}}" userInput="1:00 AM" stepKey="seeProductOptionTimeInput" /> <!--Select shipping method--> <actionGroup ref="CheckoutSelectFlatRateShippingMethodActionGroup" stepKey="selectFlatRateShippingMethod"/> @@ -121,13 +126,11 @@ <actionGroup ref="LoginAsAdmin" stepKey="loginAsAdmin1"/> - <amOnPage url="{{AdminOrdersPage.url}}" stepKey="onOrdersPage"/> - <waitForLoadingMaskToDisappear stepKey="waitForLoadingMask3"/> - <actionGroup ref="clearFiltersAdminDataGrid" stepKey="clearGridFilter"/> - <fillField selector="{{OrdersGridSection.search}}" userInput="{$grabOrderNumber}" stepKey="fillOrderNum"/> - <click selector="{{OrdersGridSection.submitSearch}}" stepKey="submitSearch"/> - <waitForLoadingMaskToDisappear stepKey="waitForLoadingMask4"/> - <click selector="{{OrdersGridSection.firstRow}}" stepKey="clickOrderRow"/> + <actionGroup ref="filterOrderGridById" stepKey="filterByOrderId"> + <argument name="orderId" value="$grabOrderNumber"/> + </actionGroup> + <click selector="{{AdminDataGridTableSection.firstRow}}" stepKey="clickOrderRow"/> + <waitForPageLoad stepKey="waitForOrderPageOpened"/> <!-- Checking the correctness of displayed custom options for user parameters on Order --> @@ -138,8 +141,8 @@ <see selector="{{AdminOrderItemsOrderedSection.productNameOptions}}" userInput="{{ProductOptionValueRadioButtons1.title}}" stepKey="seeAdminOrderProductOptionValueRadioButton1"/> <see selector="{{AdminOrderItemsOrderedSection.productNameOptions}}" userInput="{{ProductOptionValueCheckbox.title}}" stepKey="seeAdminOrderProductOptionValueCheckbox" /> <see selector="{{AdminOrderItemsOrderedSection.productNameOptions}}" userInput="{{ProductOptionValueMultiSelect1.title}}" stepKey="seeAdminOrderproductAttributeOptionsMultiselect1" /> - <see selector="{{AdminOrderItemsOrderedSection.productNameOptions}}" userInput="Jan 1, 2018" stepKey="seeAdminOrderProductOptionDateAndTime" /> - <see selector="{{AdminOrderItemsOrderedSection.productNameOptions}}" userInput="1/1/18, 1:00 AM" stepKey="seeAdminOrderProductOptionData" /> + <see selector="{{AdminOrderItemsOrderedSection.productNameOptions}}" userInput="Jan 1, $year" stepKey="seeAdminOrderProductOptionDateAndTime" /> + <see selector="{{AdminOrderItemsOrderedSection.productNameOptions}}" userInput="1/1/$shortYear, 1:00 AM" stepKey="seeAdminOrderProductOptionData" /> <see selector="{{AdminOrderItemsOrderedSection.productNameOptions}}" userInput="1:00 AM" stepKey="seeAdminOrderProductOptionTime" /> <!-- Reorder and Checking the correctness of displayed custom options for user parameters on Order and correctness of displayed price Subtotal--> @@ -155,8 +158,8 @@ <see selector="{{AdminOrderItemsOrderedSection.productNameOptions}}" userInput="{{ProductOptionValueRadioButtons1.title}}" stepKey="seeAdminOrderProductOptionValueRadioButton11"/> <see selector="{{AdminOrderItemsOrderedSection.productNameOptions}}" userInput="{{ProductOptionValueCheckbox.title}}" stepKey="seeAdminOrderProductOptionValueCheckbox1" /> <see selector="{{AdminOrderItemsOrderedSection.productNameOptions}}" userInput="{{ProductOptionValueMultiSelect1.title}}" stepKey="seeAdminOrderproductAttributeOptionsMultiselect11" /> - <see selector="{{AdminOrderItemsOrderedSection.productNameOptions}}" userInput="Jan 1, 2018" stepKey="seeAdminOrderProductOptionDateAndTime1" /> - <see selector="{{AdminOrderItemsOrderedSection.productNameOptions}}" userInput="1/1/18, 1:00 AM" stepKey="seeAdminOrderProductOptionData1" /> + <see selector="{{AdminOrderItemsOrderedSection.productNameOptions}}" userInput="Jan 1, $year" stepKey="seeAdminOrderProductOptionDateAndTime1" /> + <see selector="{{AdminOrderItemsOrderedSection.productNameOptions}}" userInput="1/1/$shortYear, 1:00 AM" stepKey="seeAdminOrderProductOptionData1" /> <see selector="{{AdminOrderItemsOrderedSection.productNameOptions}}" userInput="1:00 AM" stepKey="seeAdminOrderProductOptionTime1" /> <see selector="{{AdminOrderTotalSection.subTotal}}" userInput="{$finalProductPrice}" stepKey="seeOrderSubTotal"/> @@ -172,10 +175,13 @@ <see selector="{{StorefrontCustomerOrderSection.productCustomOptions($createProduct.name$, ProductOptionRadiobutton.title, ProductOptionValueRadioButtons1.title)}}" userInput="{{ProductOptionValueRadioButtons1.title}}" stepKey="seeStorefontOrderProductOptionValueRadioButtons11"/> <see selector="{{StorefrontCustomerOrderSection.productCustomOptions($createProduct.name$, ProductOptionCheckbox.title, ProductOptionValueCheckbox.title)}}" userInput="{{ProductOptionValueCheckbox.title}}" stepKey="seeStorefontOrderProductOptionValueCheckbox1" /> <see selector="{{StorefrontCustomerOrderSection.productCustomOptions($createProduct.name$, ProductOptionMultiSelect.title, ProductOptionValueMultiSelect1.title)}}" userInput="{{ProductOptionValueMultiSelect1.title}}" stepKey="seeStorefontOrderproductAttributeOptionsMultiselect11" /> - <see selector="{{StorefrontCustomerOrderSection.productCustomOptions($createProduct.name$, ProductOptionDate.title, 'Jan 1, 2018')}}" userInput="Jan 1, 2018" stepKey="seeStorefontOrderProductOptionDateAndTime1" /> - <see selector="{{StorefrontCustomerOrderSection.productCustomOptions($createProduct.name$, ProductOptionDateTime.title, '1/1/18, 1:00 AM')}}" userInput="1/1/18, 1:00 AM" stepKey="seeStorefontOrderProductOptionData1" /> + <see selector="{{StorefrontCustomerOrderSection.productCustomOptions($createProduct.name$, ProductOptionDate.title, 'Jan 1, $year')}}" userInput="Jan 1, $year" stepKey="seeStorefontOrderProductOptionDateAndTime1" /> + <see selector="{{StorefrontCustomerOrderSection.productCustomOptions($createProduct.name$, ProductOptionDateTime.title, '1/1/$shortYear, 1:00 AM')}}" userInput="1/1/$shortYear, 1:00 AM" stepKey="seeStorefontOrderProductOptionData1" /> <see selector="{{StorefrontCustomerOrderSection.productCustomOptions($createProduct.name$, ProductOptionTime.title, '1:00 AM')}}" userInput="1:00 AM" stepKey="seeStorefontOrderProductOptionTime1" /> + <!-- Logout customer --> + <actionGroup ref="CustomerLogoutStorefrontActionGroup" stepKey="logoutCustomer"/> + <!-- Delete product and category --> <deleteData createDataKey="createProduct" stepKey="deleteProduct"/> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/StorefrontRememberCategoryPaginationTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/StorefrontRememberCategoryPaginationTest.xml new file mode 100644 index 0000000000000..9ded627056600 --- /dev/null +++ b/app/code/Magento/Catalog/Test/Mftf/Test/StorefrontRememberCategoryPaginationTest.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="StorefrontRememberCategoryPaginationTest"> + <annotations> + <title value="Verify that Number of Products per page retained when visiting a different category"/> + <stories value="MAGETWO-73687: Number of Products displayed per page not retained when visiting a different category"/> + <description value="Verify that Number of Products per page retained when visiting a different category"/> + <features value="Catalog"/> + <severity value="MAJOR"/> + <testCaseId value="MAGETWO-96307"/> + <group value="catalog"/> + </annotations> + + <before> + <createData entity="_defaultCategory" stepKey="createCategory"/> + <createData entity="SimpleProduct" stepKey="createProduct"> + <requiredEntity createDataKey="createCategory"/> + </createData> + + <createData entity="_defaultCategory" stepKey="createCategory1"/> + <createData entity="SimpleProduct" stepKey="createProduct1"> + <requiredEntity createDataKey="createCategory1"/> + </createData> + + <createData entity="RememberPaginationCatalogStorefrontConfig" stepKey="setRememberPaginationCatalogStorefrontConfig"/> + <magentoCLI command="cache:flush" stepKey="clearCache"/> + </before> + + <after> + <createData entity="DefaultCatalogStorefrontConfiguration" stepKey="setDefaultCatalogStorefrontConfiguration"/> + + <deleteData createDataKey="createProduct" stepKey="deleteProduct"/> + <deleteData createDataKey="createCategory" stepKey="deleteCategory"/> + <deleteData createDataKey="createProduct1" stepKey="deleteProduct1"/> + <deleteData createDataKey="createCategory1" stepKey="deleteCategory1"/> + + <magentoCLI command="cache:flush" stepKey="flushCache"/> + </after> + + <actionGroup ref="GoToStorefrontCategoryPageByParameters" stepKey="goToStorefrontCategory1Page"> + <argument name="category" value="$$createCategory.name$$"/> + <argument name="mode" value="grid"/> + </actionGroup> + + <selectOption selector="{{StorefrontCategoryPagerSection.perPage}}" userInput="12" stepKey="setPerPage" /> + <waitForPageLoad stepKey="waitForPageLoad"/> + + <actionGroup ref="VerifyCategoryPageParameters" stepKey="verifyCategoryPageParameters"> + <argument name="categoryName" value="$$createCategory.name$$"/> + <argument name="mode" value="grid"/> + <argument name="numOfProductsPerPage" value="12"/> + </actionGroup> + + <amOnPage url="{{StorefrontCategoryPage.url($$createCategory1.name$$)}}" stepKey="navigateToCategory1Page"/> + <waitForPageLoad stepKey="waitForCategory1PageToLoad"/> + + <actionGroup ref="VerifyCategoryPageParameters" stepKey="verifyCategory1PageParameters"> + <argument name="categoryName" value="$$createCategory1.name$$"/> + <argument name="mode" value="grid"/> + <argument name="numOfProductsPerPage" value="12"/> + </actionGroup> + </test> +</tests> diff --git a/app/code/Magento/Catalog/Test/Unit/Block/Product/ProductList/ToolbarTest.php b/app/code/Magento/Catalog/Test/Unit/Block/Product/ProductList/ToolbarTest.php index ac963326dbfa1..1a6c25bbce2d0 100644 --- a/app/code/Magento/Catalog/Test/Unit/Block/Product/ProductList/ToolbarTest.php +++ b/app/code/Magento/Catalog/Test/Unit/Block/Product/ProductList/ToolbarTest.php @@ -18,6 +18,11 @@ class ToolbarTest extends \PHPUnit\Framework\TestCase */ protected $model; + /** + * @var \Magento\Catalog\Model\Product\ProductList\ToolbarMemorizer | \PHPUnit_Framework_MockObject_MockObject + */ + private $memorizer; + /** * @var \Magento\Framework\Url | \PHPUnit_Framework_MockObject_MockObject */ @@ -62,6 +67,16 @@ protected function setUp() 'getLimit', 'getCurrentPage' ]); + $this->memorizer = $this->createPartialMock( + \Magento\Catalog\Model\Product\ProductList\ToolbarMemorizer::class, + [ + 'getDirection', + 'getOrder', + 'getMode', + 'getLimit', + 'isMemorizingAllowed', + ] + ); $this->layout = $this->createPartialMock(\Magento\Framework\View\Layout::class, ['getChildName', 'getBlock']); $this->pagerBlock = $this->createPartialMock(\Magento\Theme\Block\Html\Pager::class, [ 'setUseContainer', @@ -116,6 +131,7 @@ protected function setUp() 'context' => $context, 'catalogConfig' => $this->catalogConfig, 'toolbarModel' => $this->model, + 'toolbarMemorizer' => $this->memorizer, 'urlEncoder' => $this->urlEncoder, 'productListHelper' => $this->productListHelper ] @@ -155,7 +171,7 @@ public function testGetPagerEncodedUrl() public function testGetCurrentOrder() { $order = 'price'; - $this->model->expects($this->once()) + $this->memorizer->expects($this->once()) ->method('getOrder') ->will($this->returnValue($order)); $this->catalogConfig->expects($this->once()) @@ -169,7 +185,7 @@ public function testGetCurrentDirection() { $direction = 'desc'; - $this->model->expects($this->once()) + $this->memorizer->expects($this->once()) ->method('getDirection') ->will($this->returnValue($direction)); @@ -183,7 +199,7 @@ public function testGetCurrentMode() $this->productListHelper->expects($this->once()) ->method('getAvailableViewMode') ->will($this->returnValue(['list' => 'List'])); - $this->model->expects($this->once()) + $this->memorizer->expects($this->once()) ->method('getMode') ->will($this->returnValue($mode)); @@ -232,11 +248,11 @@ public function testGetLimit() $mode = 'list'; $limit = 10; - $this->model->expects($this->once()) + $this->memorizer->expects($this->once()) ->method('getMode') ->will($this->returnValue($mode)); - $this->model->expects($this->once()) + $this->memorizer->expects($this->once()) ->method('getLimit') ->will($this->returnValue($limit)); $this->productListHelper->expects($this->once()) @@ -266,7 +282,7 @@ public function testGetPagerHtml() $this->productListHelper->expects($this->exactly(2)) ->method('getAvailableLimit') ->will($this->returnValue([10 => 10, 20 => 20])); - $this->model->expects($this->once()) + $this->memorizer->expects($this->once()) ->method('getLimit') ->will($this->returnValue($limit)); $this->pagerBlock->expects($this->once()) diff --git a/app/code/Magento/Catalog/Test/Unit/Model/Category/Product/PositionResolverTest.php b/app/code/Magento/Catalog/Test/Unit/Model/Category/Product/PositionResolverTest.php index 9545e5eb4b37d..a2424c7521e18 100644 --- a/app/code/Magento/Catalog/Test/Unit/Model/Category/Product/PositionResolverTest.php +++ b/app/code/Magento/Catalog/Test/Unit/Model/Category/Product/PositionResolverTest.php @@ -105,7 +105,7 @@ public function testGetPositions() $this->select->expects($this->once()) ->method('where') ->willReturnSelf(); - $this->select->expects($this->once()) + $this->select->expects($this->exactly(2)) ->method('order') ->willReturnSelf(); $this->select->expects($this->once()) diff --git a/app/code/Magento/Catalog/Test/Unit/Model/Indexer/Product/Flat/Action/RowTest.php b/app/code/Magento/Catalog/Test/Unit/Model/Indexer/Product/Flat/Action/RowTest.php index f095867ed5c39..2de3b4ddf3f50 100644 --- a/app/code/Magento/Catalog/Test/Unit/Model/Indexer/Product/Flat/Action/RowTest.php +++ b/app/code/Magento/Catalog/Test/Unit/Model/Indexer/Product/Flat/Action/RowTest.php @@ -100,10 +100,10 @@ protected function setUp() ->disableOriginalConstructor() ->getMock(); $this->connection->expects($this->any())->method('select')->willReturn($selectMock); - $selectMock->expects($this->any())->method('from')->with( - $attributeTable, - ['value'] - )->willReturnSelf(); + $selectMock->method('from') + ->willReturnSelf(); + $selectMock->method('joinLeft') + ->willReturnSelf(); $selectMock->expects($this->any())->method('where')->willReturnSelf(); $selectMock->expects($this->any())->method('order')->willReturnSelf(); $selectMock->expects($this->any())->method('limit')->willReturnSelf(); diff --git a/app/code/Magento/Catalog/Test/Unit/Model/Product/ProductList/ToolbarMemorizerTest.php b/app/code/Magento/Catalog/Test/Unit/Model/Product/ProductList/ToolbarMemorizerTest.php new file mode 100644 index 0000000000000..5cb341a36b4cc --- /dev/null +++ b/app/code/Magento/Catalog/Test/Unit/Model/Product/ProductList/ToolbarMemorizerTest.php @@ -0,0 +1,213 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +namespace Magento\Catalog\Test\Unit\Model\Product\ProductList; + +use Magento\Catalog\Model\Product\ProductList\Toolbar; +use Magento\Catalog\Model\Product\ProductList\ToolbarMemorizer; +use Magento\Framework\TestFramework\Unit\Helper\ObjectManager; +use Magento\Catalog\Model\Session as CatalogSession; +use Magento\Framework\App\Config\ScopeConfigInterface; + +/** + * Class for testing toolbal memorizer. + */ +class ToolbarMemorizerTest extends \PHPUnit\Framework\TestCase +{ + /** + * @var ToolbarMemorizer + */ + private $model; + + /** + * @var \PHPUnit_Framework_MockObject_MockObject|Toolbar + */ + private $toolbarMock; + + /** + * @var \PHPUnit_Framework_MockObject_MockObject|CatalogSession + */ + private $catalogSessionMock; + + /** + * @var \PHPUnit_Framework_MockObject_MockObject|ScopeConfigInterface + */ + private $scopeConfigMock; + + /** + * @var ObjectManager $objectManager + */ + private $objectManager; + + /** + * @inheritdoc + */ + protected function setUp() + { + $this->toolbarMock = $this->getMockBuilder(Toolbar::class) + ->disableOriginalConstructor() + ->setMethods(['getOrder', 'getDirection', 'getLimit', 'getMode']) + ->getMock(); + $this->catalogSessionMock = $this->getMockBuilder(CatalogSession::class) + ->disableOriginalConstructor() + ->setMethods(['getParamsMemorizeDisabled', 'getData']) + ->getMock(); + $this->scopeConfigMock = $this->getMockBuilder(ScopeConfigInterface::class) + ->disableOriginalConstructor() + ->getMock(); + + $this->objectManager = new ObjectManager($this); + $this->model = $this->objectManager->getObject( + ToolbarMemorizer::class, + [ + 'toolbarModel' => $this->toolbarMock, + 'catalogSession' => $this->catalogSessionMock, + 'scopeConfig' => $this->scopeConfigMock, + ] + ); + } + + /** + * @return array + */ + public function getMainDataProvider(): array + { + return [ + ['any_value',null,null,null,'any_value'], + [null, 'any_value', false, null, 'any_value'], + [null, null, false, null, null], + [null, null, true, 'data', 'data'], + ]; + } + + /** + * Test get order. + * + * @param string|null $variable + * @param string|null $variableValue + * @param bool|null $flag + * @param string|null $data + * @param string|null $expected + * @return void + * + * @dataProvider getMainDataProvider + */ + public function testGetOrder($variable, $variableValue, $flag, $data, $expected) + { + $this->objectManager->setBackwardCompatibleProperty($this->model, 'order', $variable); + $this->toolbarMock->method('getOrder')->willReturn($variableValue); + $this->scopeConfigMock->method('isSetFlag')->willReturn($flag); + $this->catalogSessionMock->method('getData')->willReturn($data); + $this->assertEquals($expected, $this->model->getOrder()); + } + + /** + * Test get direction. + * + * @param string|null $variable + * @param string|null $variableValue + * @param bool|null $flag + * @param string|null $data + * @param string|null $expected + * @return void + * + * @dataProvider getMainDataProvider + */ + public function testGetDirection($variable, $variableValue, $flag, $data, $expected) + { + $this->objectManager->setBackwardCompatibleProperty($this->model, 'direction', $variable); + $this->toolbarMock->method('getDirection')->willReturn($variableValue); + $this->scopeConfigMock->method('isSetFlag')->willReturn($flag); + $this->catalogSessionMock->method('getData')->willReturn($data); + $this->assertEquals($expected, $this->model->getDirection()); + } + + /** + * Test get mode. + * + * @param string|null $variable + * @param string|null $variableValue + * @param bool|null $flag + * @param string|null $data + * @param string|null $expected + * @return void + * + * @dataProvider getMainDataProvider + */ + public function testGetMode($variable, $variableValue, $flag, $data, $expected) + { + $this->objectManager->setBackwardCompatibleProperty($this->model, 'mode', $variable); + $this->toolbarMock->method('getMode')->willReturn($variableValue); + $this->scopeConfigMock->method('isSetFlag')->willReturn($flag); + $this->catalogSessionMock->method('getData')->willReturn($data); + $this->assertEquals($expected, $this->model->getMode()); + } + + /** + * Test getting limit. + * + * @param string|null $variable + * @param string|null $variableValue + * @param bool|null $flag + * @param string|null $data + * @param string|null $expected + * @return void + * + * @dataProvider getMainDataProvider + */ + public function testGetLimit($variable, $variableValue, $flag, $data, $expected) + { + $this->objectManager->setBackwardCompatibleProperty($this->model, 'limit', $variable); + $this->toolbarMock->method('getLimit')->willReturn($variableValue); + $this->scopeConfigMock->method('isSetFlag')->willReturn($flag); + $this->catalogSessionMock->method('getData')->willReturn($data); + $this->assertEquals($expected, $this->model->getLimit()); + } + + /** + * Test memorizing parameters. + * + * @return void + */ + public function testMemorizeParams() + { + $this->catalogSessionMock->method('getParamsMemorizeDisabled')->willReturn(false); + $this->objectManager->setBackwardCompatibleProperty($this->model, 'isMemorizingAllowed', true); + $this->model->memorizeParams(); + } + + /** + * @return array + */ + public function getMemorizedDataProvider(): array + { + return [ + [null, false, false], + [null, true, true], + [false, false, false], + [false, true, false], + [true, false, true], + [true, true, true], + ]; + } + + /** + * Test method isMemorizingAllowed. + * + * @aram bool|null $variableValue + * @param bool $flag + * @param bool $expected + * @return void + * + * @dataProvider getMemorizedDataProvider + */ + public function testIsMemorizingAllowed($variableValue, bool $flag, bool $expected) + { + $this->objectManager->setBackwardCompatibleProperty($this->model, 'isMemorizingAllowed', $variableValue); + $this->scopeConfigMock->method('isSetFlag')->willReturn($flag); + $this->assertEquals($expected, $this->model->isMemorizingAllowed()); + } +} diff --git a/app/code/Magento/Catalog/Test/Unit/Model/ProductTest.php b/app/code/Magento/Catalog/Test/Unit/Model/ProductTest.php index 483283e777118..677a45c41f846 100644 --- a/app/code/Magento/Catalog/Test/Unit/Model/ProductTest.php +++ b/app/code/Magento/Catalog/Test/Unit/Model/ProductTest.php @@ -543,6 +543,7 @@ public function testSetCategoryCollection() public function testGetCategory() { + $this->model->setData('category_ids', [10]); $this->category->expects($this->any())->method('getId')->will($this->returnValue(10)); $this->registry->expects($this->any())->method('registry')->will($this->returnValue($this->category)); $this->categoryRepository->expects($this->any())->method('get')->will($this->returnValue($this->category)); @@ -551,7 +552,8 @@ public function testGetCategory() public function testGetCategoryId() { - $this->category->expects($this->once())->method('getId')->will($this->returnValue(10)); + $this->model->setData('category_ids', [10]); + $this->category->expects($this->any())->method('getId')->will($this->returnValue(10)); $this->registry->expects($this->at(0))->method('registry'); $this->registry->expects($this->at(1))->method('registry')->will($this->returnValue($this->category)); @@ -559,6 +561,14 @@ public function testGetCategoryId() $this->assertEquals(10, $this->model->getCategoryId()); } + public function testGetCategoryIdWhenProductNotInCurrentCategory() + { + $this->model->setData('category_ids', [12]); + $this->category->expects($this->once())->method('getId')->will($this->returnValue(10)); + $this->registry->expects($this->any())->method('registry')->will($this->returnValue($this->category)); + $this->assertFalse($this->model->getCategoryId()); + } + public function testGetIdBySku() { $this->resource->expects($this->once())->method('getIdBySku')->will($this->returnValue(5)); diff --git a/app/code/Magento/Catalog/Test/Unit/Model/ResourceModel/Product/ImageTest.php b/app/code/Magento/Catalog/Test/Unit/Model/ResourceModel/Product/ImageTest.php new file mode 100644 index 0000000000000..44f66b6cbf66e --- /dev/null +++ b/app/code/Magento/Catalog/Test/Unit/Model/ResourceModel/Product/ImageTest.php @@ -0,0 +1,237 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Catalog\Test\Unit\Model\ResourceModel\Product; + +use Magento\Catalog\Model\ResourceModel\Product\Image; +use Magento\Framework\DB\Adapter\AdapterInterface; +use Magento\Framework\DB\Query\Generator; +use Magento\Framework\DB\Select; +use Magento\Framework\App\ResourceConnection; +use Magento\Catalog\Model\ResourceModel\Product\Gallery; +use PHPUnit_Framework_MockObject_MockObject as MockObject; +use Magento\Framework\DB\Query\BatchIteratorInterface; + +class ImageTest extends \PHPUnit\Framework\TestCase +{ + /** + * @var \Magento\Framework\TestFramework\Unit\Helper\ObjectManager + */ + protected $objectManager; + + /** + * @var AdapterInterface | MockObject + */ + protected $connectionMock; + + /** + * @var Generator | MockObject + */ + protected $generatorMock; + + /** + * @var ResourceConnection | MockObject + */ + protected $resourceMock; + + protected function setUp() + { + $this->objectManager = + new \Magento\Framework\TestFramework\Unit\Helper\ObjectManager($this); + $this->connectionMock = $this->createMock(AdapterInterface::class); + $this->resourceMock = $this->createMock(ResourceConnection::class); + $this->resourceMock->method('getConnection') + ->willReturn($this->connectionMock); + $this->resourceMock->method('getTableName') + ->willReturnArgument(0); + $this->generatorMock = $this->createMock(Generator::class); + } + + /** + * @return MockObject + */ + protected function getVisibleImagesSelectMock(): MockObject + { + $selectMock = $this->getMockBuilder(Select::class) + ->disableOriginalConstructor() + ->getMock(); + $selectMock->expects($this->once()) + ->method('distinct') + ->willReturnSelf(); + $selectMock->expects($this->once()) + ->method('from') + ->with( + ['images' => Gallery::GALLERY_TABLE], + 'value as filepath' + )->willReturnSelf(); + $selectMock->expects($this->once()) + ->method('where') + ->with('disabled = 0') + ->willReturnSelf(); + + return $selectMock; + } + + /** + * @param int $imagesCount + * @dataProvider dataProvider + */ + public function testGetCountAllProductImages(int $imagesCount) + { + $selectMock = $this->getVisibleImagesSelectMock(); + $selectMock->expects($this->exactly(2)) + ->method('reset') + ->withConsecutive( + ['columns'], + ['distinct'] + )->willReturnSelf(); + $selectMock->expects($this->once()) + ->method('columns') + ->with(new \Zend_Db_Expr('count(distinct value)')) + ->willReturnSelf(); + + $this->connectionMock->expects($this->once()) + ->method('select') + ->willReturn($selectMock); + $this->connectionMock->expects($this->once()) + ->method('fetchOne') + ->with($selectMock) + ->willReturn($imagesCount); + + $imageModel = $this->objectManager->getObject( + Image::class, + [ + 'generator' => $this->generatorMock, + 'resourceConnection' => $this->resourceMock + ] + ); + + $this->assertSame( + $imagesCount, + $imageModel->getCountAllProductImages() + ); + } + + /** + * @param int $imagesCount + * @param int $batchSize + * @dataProvider dataProvider + */ + public function testGetAllProductImages( + int $imagesCount, + int $batchSize + ) { + $this->connectionMock->expects($this->once()) + ->method('select') + ->willReturn($this->getVisibleImagesSelectMock()); + + $batchCount = (int)ceil($imagesCount / $batchSize); + $fetchResultsCallback = $this->getFetchResultCallbackForBatches($imagesCount, $batchSize); + $this->connectionMock->expects($this->exactly($batchCount)) + ->method('fetchAll') + ->will($this->returnCallback($fetchResultsCallback)); + + /** @var Select | MockObject $selectMock */ + $selectMock = $this->getMockBuilder(Select::class) + ->disableOriginalConstructor() + ->getMock(); + + $this->generatorMock->expects($this->once()) + ->method('generate') + ->with( + 'value_id', + $selectMock, + $batchSize, + BatchIteratorInterface::NON_UNIQUE_FIELD_ITERATOR + )->will( + $this->returnCallback( + $this->getBatchIteratorCallback($selectMock, $batchCount) + ) + ); + + $imageModel = $this->objectManager->getObject( + Image::class, + [ + 'generator' => $this->generatorMock, + 'resourceConnection' => $this->resourceMock, + 'batchSize' => $batchSize + ] + ); + + $this->assertCount($imagesCount, $imageModel->getAllProductImages()); + } + + /** + * @param int $imagesCount + * @param int $batchSize + * @return \Closure + */ + protected function getFetchResultCallbackForBatches( + int $imagesCount, + int $batchSize + ): \Closure { + $fetchResultsCallback = function () use (&$imagesCount, $batchSize) { + $batchSize = + ($imagesCount >= $batchSize) ? $batchSize : $imagesCount; + $imagesCount -= $batchSize; + + $getFetchResults = function ($batchSize): array { + $result = []; + $count = $batchSize; + while ($count) { + $count--; + $result[$count] = $count; + } + + return $result; + }; + + return $getFetchResults($batchSize); + }; + + return $fetchResultsCallback; + } + + /** + * @param Select | MockObject $selectMock + * @param int $batchCount + * @return \Closure + */ + protected function getBatchIteratorCallback( + MockObject $selectMock, + int $batchCount + ): \Closure { + $iteratorCallback = function () use ($batchCount, $selectMock): array { + $result = []; + $count = $batchCount; + while ($count) { + $count--; + $result[$count] = $selectMock; + } + + return $result; + }; + + return $iteratorCallback; + } + + /** + * Data Provider + * @return array + */ + public function dataProvider(): array + { + return [ + [300, 300], + [300, 100], + [139, 100], + [67, 10], + [154, 47], + [0, 100] + ]; + } +} diff --git a/app/code/Magento/Catalog/Test/Unit/Plugin/Framework/App/Action/ContextPluginTest.php b/app/code/Magento/Catalog/Test/Unit/Plugin/Framework/App/Action/ContextPluginTest.php new file mode 100644 index 0000000000000..efd78cac6e512 --- /dev/null +++ b/app/code/Magento/Catalog/Test/Unit/Plugin/Framework/App/Action/ContextPluginTest.php @@ -0,0 +1,78 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +namespace Magento\Catalog\Test\Unit\Plugin\Framework\App\Action; + +use Magento\Framework\TestFramework\Unit\Helper\ObjectManager as ObjectManagerHelper; +use Magento\Catalog\Plugin\Framework\App\Action\ContextPlugin; +use Magento\Catalog\Model\Product\ProductList\ToolbarMemorizer; +use Magento\Catalog\Model\Session as CatalogSession; +use Magento\Framework\App\Http\Context as HttpContext; + +/** + * Class for testing ContextPlugin class. + */ +class ContextPluginTest extends \PHPUnit\Framework\TestCase +{ + /** + * @var ContextPlugin + */ + private $model; + + /** + * @var \PHPUnit_Framework_MockObject_MockObject|ToolbarMemorizer + */ + private $toolbarMemorizerMock; + + /** + * @var \PHPUnit_Framework_MockObject_MockObject|CatalogSession + */ + private $catalogSessionMock; + + /** + * @var \PHPUnit_Framework_MockObject_MockObject|HttpContext + */ + private $httpContextMock; + + /** + * @inheritdoc + */ + protected function setUp() + { + $this->toolbarMemorizerMock = $this->getMockBuilder(ToolbarMemorizer::class) + ->disableOriginalConstructor() + ->getMock(); + $this->catalogSessionMock = $this->getMockBuilder(CatalogSession::class) + ->disableOriginalConstructor() + ->getMock(); + $this->httpContextMock = $this->getMockBuilder(HttpContext::class) + ->disableOriginalConstructor() + ->getMock(); + + $objectManagerHelper = new ObjectManagerHelper($this); + $this->model = $objectManagerHelper->getObject( + ContextPlugin::class, + [ + 'toolbarMemorizer' => $this->toolbarMemorizerMock, + 'catalogSession' => $this->catalogSessionMock, + 'httpContext' => $this->httpContextMock, + ] + ); + } + + /** + * Test beforeDispatch method. + * + * @return void + */ + public function testBeforeDispatch() + { + $this->toolbarMemorizerMock->method('isMemorizingAllowed')->willReturn(true); + $this->catalogSessionMock->method('getData')->willReturn('any_value'); + + $this->model->beforeDispatch(); + } +} diff --git a/app/code/Magento/Catalog/Test/Unit/Ui/DataProvider/Product/Listing/Collector/ImageTest.php b/app/code/Magento/Catalog/Test/Unit/Ui/DataProvider/Product/Listing/Collector/ImageTest.php index 12bc9acfa4c51..2d6b082e35b17 100644 --- a/app/code/Magento/Catalog/Test/Unit/Ui/DataProvider/Product/Listing/Collector/ImageTest.php +++ b/app/code/Magento/Catalog/Test/Unit/Ui/DataProvider/Product/Listing/Collector/ImageTest.php @@ -6,15 +6,16 @@ namespace Magento\Catalog\Test\Unit\Ui\DataProvider\Product\Listing\Collector; +use Magento\Catalog\Api\Data\ProductInterface; +use Magento\Catalog\Api\Data\ProductRender\ImageInterface; use Magento\Catalog\Api\Data\ProductRenderInterface; +use Magento\Catalog\Helper\Image as ImageHelper; +use Magento\Catalog\Helper\ImageFactory; use Magento\Catalog\Model\Product; -use Magento\Catalog\Api\Data\ProductInterface; use Magento\Catalog\Ui\DataProvider\Product\Listing\Collector\Image; use Magento\Framework\View\DesignInterface; +use Magento\Framework\View\DesignLoader; use Magento\Store\Model\StoreManagerInterface; -use Magento\Catalog\Helper\ImageFactory; -use Magento\Catalog\Api\Data\ProductRender\ImageInterface; -use Magento\Catalog\Helper\Image as ImageHelper; /** * @SuppressWarnings(PHPMD.CouplingBetweenObjects) @@ -42,8 +43,21 @@ class ImageTest extends \PHPUnit\Framework\TestCase /** @var \Magento\Catalog\Api\Data\ProductRender\ImageInterfaceFactory|\PHPUnit_Framework_MockObject_MockObject */ private $imageInterfaceFactory; + /** @var DesignLoader|\PHPUnit_Framework_MockObject_MockObject */ + private $designLoader; + + /** + * @var \Magento\Framework\TestFramework\Unit\Helper\ObjectManager + */ + protected $objectManager; + + /** + * @inheritdoc + */ public function setUp() { + $this->objectManager = new \Magento\Framework\TestFramework\Unit\Helper\ObjectManager($this); + $this->imageFactory = $this->getMockBuilder(ImageFactory::class) ->disableOriginalConstructor() ->getMock(); @@ -60,14 +74,21 @@ public function setUp() ->getMock(); $this->storeManager = $this->createMock(StoreManagerInterface::class); $this->design = $this->createMock(DesignInterface::class); - $this->model = new Image( - $this->imageFactory, - $this->state, - $this->storeManager, - $this->design, - $this->imageInterfaceFactory, - $this->imageCodes - ); + $this->designLoader = $this->createMock(DesignLoader::class); + + $this->model = $this->objectManager + ->getObject( + Image::class, + [ + 'imageFactory' => $this->imageFactory, + 'state' => $this->state, + 'storeManager' => $this->storeManager, + 'design' => $this->design, + 'imageRenderInfoFactory' => $this->imageInterfaceFactory, + 'imageCodes' => $this->imageCodes, + 'designLoader' => $this->designLoader, + ] + ); } public function testGet() @@ -165,6 +186,7 @@ public function testEmulateImageCreating() $imageMock->expects($this->once()) ->method('setUrl') ->with('url'); + $this->designLoader->expects($this->once())->method('load'); $this->assertEquals( $imageHelperMock, 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 0eddca3322205..1a9b9f205d701 100644 --- a/app/code/Magento/Catalog/Ui/DataProvider/Product/Form/Modifier/TierPrice.php +++ b/app/code/Magento/Catalog/Ui/DataProvider/Product/Form/Modifier/TierPrice.php @@ -45,7 +45,7 @@ public function __construct( } /** - * {@inheritdoc} + * @inheritdoc * @since 101.1.0 */ public function modifyData(array $data) @@ -54,8 +54,11 @@ public function modifyData(array $data) } /** - * {@inheritdoc} + * Add tier price info to meta array. + * * @since 101.1.0 + * @param array $meta + * @return array */ public function modifyMeta(array $meta) { @@ -150,8 +153,8 @@ private function getUpdatedTierPriceStructure(array $priceMeta) 'dataType' => Price::NAME, 'addbefore' => '%', 'validation' => [ - 'validate-number' => true, - 'less-than-equals-to' => 100 + 'required-entry' => true, + 'validate-positive-percent-decimal' => true, ], 'visible' => $firstOption && $firstOption['value'] == ProductPriceOptionsInterface::VALUE_PERCENT, diff --git a/app/code/Magento/Catalog/Ui/DataProvider/Product/Listing/Collector/Image.php b/app/code/Magento/Catalog/Ui/DataProvider/Product/Listing/Collector/Image.php index 216bc16968fcb..34879ab29c185 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 @@ -17,12 +17,14 @@ use Magento\Framework\View\DesignInterface; use Magento\Store\Model\StoreManager; use Magento\Store\Model\StoreManagerInterface; +use Magento\Framework\View\DesignLoader; /** * Collect enough information about image rendering on front * If you want to add new image, that should render on front you need * to configure this class in di.xml * + * @SuppressWarnings(PHPMD.CouplingBetweenObjects) */ class Image implements ProductRenderCollectorInterface { @@ -59,6 +61,11 @@ class Image implements ProductRenderCollectorInterface */ private $imageRenderInfoFactory; + /** + * @var DesignLoader + */ + private $designLoader; + /** * Image constructor. * @param ImageFactory $imageFactory @@ -67,6 +74,7 @@ class Image implements ProductRenderCollectorInterface * @param DesignInterface $design * @param ImageInterfaceFactory $imageRenderInfoFactory * @param array $imageCodes + * @param DesignLoader|null $designLoader */ public function __construct( ImageFactory $imageFactory, @@ -74,7 +82,8 @@ public function __construct( StoreManagerInterface $storeManager, DesignInterface $design, ImageInterfaceFactory $imageRenderInfoFactory, - array $imageCodes = [] + array $imageCodes = [], + DesignLoader $designLoader = null ) { $this->imageFactory = $imageFactory; $this->imageCodes = $imageCodes; @@ -82,6 +91,8 @@ public function __construct( $this->storeManager = $storeManager; $this->design = $design; $this->imageRenderInfoFactory = $imageRenderInfoFactory; + $this->designLoader = $designLoader ?: \Magento\Framework\App\ObjectManager::getInstance() + ->get(DesignLoader::class); } /** @@ -124,6 +135,8 @@ public function collect(ProductInterface $product, ProductRenderInterface $produ } /** + * Callback for emulating image creation. + * * Callback in which we emulate initialize default design theme, depends on current store, be settings store id * from render info * @@ -136,7 +149,7 @@ public function collect(ProductInterface $product, ProductRenderInterface $produ public function emulateImageCreating(ProductInterface $product, $imageCode, $storeId, ImageInterface $image) { $this->storeManager->setCurrentStore($storeId); - $this->design->setDefaultDesignTheme(); + $this->designLoader->load(); $imageHelper = $this->imageFactory->create(); $imageHelper->init($product, $imageCode); diff --git a/app/code/Magento/Catalog/etc/adminhtml/system.xml b/app/code/Magento/Catalog/etc/adminhtml/system.xml index 7d62d46210ea2..9c99a72c12d1c 100644 --- a/app/code/Magento/Catalog/etc/adminhtml/system.xml +++ b/app/code/Magento/Catalog/etc/adminhtml/system.xml @@ -98,6 +98,11 @@ <comment>E.g. {{media url="path/to/image.jpg"}} {{skin url="path/to/picture.gif"}}. Dynamic directives parsing impacts catalog performance.</comment> <source_model>Magento\Config\Model\Config\Source\Yesno</source_model> </field> + <field id="remember_pagination" translate="label comment" type="select" sortOrder="7" showInDefault="1" showInWebsite="0" showInStore="0" canRestore="1"> + <label>Remember Category Pagination</label> + <comment>Changing may affect SEO and cache storage consumption.</comment> + <source_model>Magento\Config\Model\Config\Source\Yesno</source_model> + </field> </group> <group id="placeholder" translate="label" sortOrder="300" showInDefault="1" showInWebsite="1" showInStore="1"> <label>Product Image Placeholders</label> diff --git a/app/code/Magento/Catalog/etc/config.xml b/app/code/Magento/Catalog/etc/config.xml index b7733a1ab0239..fe1c8e7b87a7a 100644 --- a/app/code/Magento/Catalog/etc/config.xml +++ b/app/code/Magento/Catalog/etc/config.xml @@ -30,6 +30,7 @@ <flat_catalog_category>0</flat_catalog_category> <default_sort_by>position</default_sort_by> <parse_url_directives>1</parse_url_directives> + <remember_pagination>0</remember_pagination> </frontend> <product> <flat> diff --git a/app/code/Magento/Catalog/etc/di.xml b/app/code/Magento/Catalog/etc/di.xml index 5b87c7d6ac030..1d3e9a931b677 100644 --- a/app/code/Magento/Catalog/etc/di.xml +++ b/app/code/Magento/Catalog/etc/di.xml @@ -907,6 +907,14 @@ <type name="Magento\Quote\Model\Quote\Item\ToOrderItem"> <plugin name="copy_quote_files_to_order" type="Magento\Catalog\Model\Plugin\QuoteItemProductOption"/> </type> + <type name="Magento\Catalog\Model\ResourceModel\Category"> + <plugin name="remove_redundant_image" type="Magento\Catalog\Plugin\Model\ResourceModel\Category\RemoveRedundantImagePlugin"/> + </type> + <type name="Magento\Catalog\Plugin\Model\ResourceModel\Category\RemoveRedundantImagePlugin"> + <arguments> + <argument name="imageUploader" xsi:type="object">Magento\Catalog\CategoryImageUpload</argument> + </arguments> + </type> <preference for="Magento\Catalog\Model\ResourceModel\Product\BaseSelectProcessorInterface" type="Magento\Catalog\Model\ResourceModel\Product\CompositeWithWebsiteProcessor" /> <type name="Magento\Catalog\Model\ResourceModel\Product\CompositeBaseSelectProcessor"> <arguments> diff --git a/app/code/Magento/Catalog/etc/frontend/di.xml b/app/code/Magento/Catalog/etc/frontend/di.xml index 2e98c980f5686..95391b656380f 100644 --- a/app/code/Magento/Catalog/etc/frontend/di.xml +++ b/app/code/Magento/Catalog/etc/frontend/di.xml @@ -95,4 +95,7 @@ <plugin name="get_catalog_category_product_index_table_name" type="Magento\Catalog\Model\Indexer\Category\Product\Plugin\TableResolver"/> <plugin name="get_catalog_product_price_index_table_name" type="Magento\Catalog\Model\Indexer\Product\Price\Plugin\TableResolver"/> </type> + <type name="Magento\Framework\App\Action\AbstractAction"> + <plugin name="catalog_app_action_dispatch_controller_context_plugin" type="Magento\Catalog\Plugin\Framework\App\Action\ContextPlugin" /> + </type> </config> diff --git a/app/code/Magento/Catalog/i18n/en_US.csv b/app/code/Magento/Catalog/i18n/en_US.csv index 35a2c224c4ed2..1e0dcfa0232a7 100644 --- a/app/code/Magento/Catalog/i18n/en_US.csv +++ b/app/code/Magento/Catalog/i18n/en_US.csv @@ -798,3 +798,4 @@ Details,Details "Add To Compare","Add To Compare" "Learn more","Learn more" "Recently Viewed","Recently Viewed" +"You added product %1 to the <a href=""%2"">comparison list</a>.","You added product %1 to the <a href=""%2"">comparison list</a>." 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 350b715ac6240..78f74459838ac 100644 --- a/app/code/Magento/Catalog/view/adminhtml/templates/catalog/product/attribute/set/main.phtml +++ b/app/code/Magento/Catalog/view/adminhtml/templates/catalog/product/attribute/set/main.phtml @@ -80,7 +80,7 @@ // set the root node this.root = new Ext.tree.TreeNode({ text: 'ROOT', - allowDrug:false, + allowDrag:false, allowDrop:true, id:'1' }); diff --git a/app/code/Magento/Catalog/view/adminhtml/web/catalog/category/edit.js b/app/code/Magento/Catalog/view/adminhtml/web/catalog/category/edit.js index 75ee3019cf4b6..41f7a874c26f3 100644 --- a/app/code/Magento/Catalog/view/adminhtml/web/catalog/category/edit.js +++ b/app/code/Magento/Catalog/view/adminhtml/web/catalog/category/edit.js @@ -82,7 +82,7 @@ define([ return function (config, element) { config = config || {}; jQuery(element).on('click', function () { - categorySubmit(config.url, config.ajax); + categorySubmit(); }); }; }); diff --git a/app/code/Magento/Catalog/view/base/web/js/price-box.js b/app/code/Magento/Catalog/view/base/web/js/price-box.js index de68d769885fd..783d39cddbc76 100644 --- a/app/code/Magento/Catalog/view/base/web/js/price-box.js +++ b/app/code/Magento/Catalog/view/base/web/js/price-box.js @@ -78,11 +78,7 @@ define([ pricesCode = [], priceValue, origin, finalPrice; - if (typeof newPrices !== 'undefined' && newPrices.hasOwnProperty('prices')) { - this.cache.additionalPriceObject = {}; - } else { - this.cache.additionalPriceObject = this.cache.additionalPriceObject || {}; - } + this.cache.additionalPriceObject = this.cache.additionalPriceObject || {}; if (newPrices) { $.extend(this.cache.additionalPriceObject, newPrices); 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 88be03a04e71a..b8b6ff65be2b4 100644 --- a/app/code/Magento/Catalog/view/frontend/web/js/product/list/toolbar.js +++ b/app/code/Magento/Catalog/view/frontend/web/js/product/list/toolbar.js @@ -27,7 +27,9 @@ define([ directionDefault: 'asc', orderDefault: 'position', limitDefault: '9', - url: '' + url: '', + formKey: '', + post: false }, /** @inheritdoc */ @@ -89,7 +91,7 @@ define([ baseUrl = urlPaths[0], urlParams = urlPaths[1] ? urlPaths[1].split('&') : [], paramData = {}, - parameters, i; + parameters, i, form, params, key, input, formKey; for (i = 0; i < urlParams.length; i++) { parameters = urlParams[i].split('='); @@ -99,12 +101,38 @@ define([ } paramData[paramName] = paramValue; - if (paramValue == defaultValue) { //eslint-disable-line eqeqeq - delete paramData[paramName]; - } - paramData = $.param(paramData); + if (this.options.post) { + form = document.createElement('form'); + params = [this.options.mode, this.options.direction, this.options.order, this.options.limit]; + + for (key in paramData) { + if (params.indexOf(key) !== -1) { //eslint-disable-line max-depth + input = document.createElement('input'); + input.name = key; + input.value = paramData[key]; + form.appendChild(input); + delete paramData[key]; + } + } + formKey = document.createElement('input'); + formKey.name = 'form_key'; + formKey.value = this.options.formKey; + form.appendChild(formKey); + + paramData = $.param(paramData); + baseUrl += paramData.length ? '?' + paramData : ''; - location.href = baseUrl + (paramData.length ? '?' + paramData : ''); + form.action = baseUrl; + form.method = 'POST'; + document.body.appendChild(form); + form.submit(); + } else { + if (paramValue == defaultValue) { //eslint-disable-line eqeqeq + delete paramData[paramName]; + } + paramData = $.param(paramData); + location.href = baseUrl + (paramData.length ? '?' + paramData : ''); + } } }); diff --git a/app/code/Magento/CatalogImportExport/Model/Export/Product.php b/app/code/Magento/CatalogImportExport/Model/Export/Product.php index bf46c0efb2a74..413209626b794 100644 --- a/app/code/Magento/CatalogImportExport/Model/Export/Product.php +++ b/app/code/Magento/CatalogImportExport/Model/Export/Product.php @@ -1161,6 +1161,10 @@ protected function isValidAttributeValue($code, $value) $isValid = false; } + if (is_array($value)) { + $isValid = false; + } + return $isValid; } diff --git a/app/code/Magento/CatalogImportExport/Model/Import/Product.php b/app/code/Magento/CatalogImportExport/Model/Import/Product.php index 84fa13e6599d9..3578123e94dc1 100644 --- a/app/code/Magento/CatalogImportExport/Model/Import/Product.php +++ b/app/code/Magento/CatalogImportExport/Model/Import/Product.php @@ -1372,7 +1372,7 @@ protected function _saveProductCategories(array $categoriesData) $delProductId[] = $productId; foreach (array_keys($categories) as $categoryId) { - $categoriesIn[] = ['product_id' => $productId, 'category_id' => $categoryId, 'position' => 1]; + $categoriesIn[] = ['product_id' => $productId, 'category_id' => $categoryId, 'position' => 0]; } } if (Import::BEHAVIOR_APPEND != $this->getBehavior()) { @@ -1579,8 +1579,12 @@ protected function _saveProducts() continue; } if ($this->getErrorAggregator()->hasToBeTerminated()) { - $this->getErrorAggregator()->addRowToSkip($rowNum); - continue; + if (ProcessingErrorAggregatorInterface::VALIDATION_STRATEGY_SKIP_ERRORS + !== $this->_parameters[Import::FIELD_NAME_VALIDATION_STRATEGY] + ) { + $this->getErrorAggregator()->addRowToSkip($rowNum); + continue; + } } $rowScope = $this->getRowScope($rowData); @@ -1735,6 +1739,7 @@ protected function _saveProducts() if ($uploadedFile) { $uploadedImages[$columnImage] = $uploadedFile; } else { + unset($rowData[$column]); $this->addRowError( ValidatorInterface::ERROR_MEDIA_URL_NOT_ACCESSIBLE, $rowNum, @@ -2992,9 +2997,7 @@ private function formatStockDataForRow(array $rowData) ) ) { $stockItemDo->setData($row); - $row['is_in_stock'] = $stockItemDo->getBackorders() && isset($row['is_in_stock']) - ? $row['is_in_stock'] - : $this->stockStateProvider->verifyStock($stockItemDo); + $row['is_in_stock'] = $row['is_in_stock'] ?? $this->stockStateProvider->verifyStock($stockItemDo); if ($this->stockStateProvider->verifyNotification($stockItemDo)) { $row['low_stock_date'] = $this->dateTime->gmDate( 'Y-m-d H:i:s', diff --git a/app/code/Magento/CatalogImportExport/Model/Import/Uploader.php b/app/code/Magento/CatalogImportExport/Model/Import/Uploader.php index 0578cc41d0739..081934dfdfb14 100644 --- a/app/code/Magento/CatalogImportExport/Model/Import/Uploader.php +++ b/app/code/Magento/CatalogImportExport/Model/Import/Uploader.php @@ -191,9 +191,9 @@ public function move($fileName, $renameFileOff = false) } $fileName = preg_replace('/[^a-z0-9\._-]+/i', '', $fileName); - $filePath = $this->_directory->getRelativePath($filePath . $fileName); + $relativePath = $this->_directory->getRelativePath($filePath . $fileName); $this->_directory->writeFile( - $filePath, + $relativePath, $read->readAll() ); } diff --git a/app/code/Magento/CatalogInventory/Block/Stockqty/AbstractStockqty.php b/app/code/Magento/CatalogInventory/Block/Stockqty/AbstractStockqty.php index 568fa600ec52d..6614b418da920 100644 --- a/app/code/Magento/CatalogInventory/Block/Stockqty/AbstractStockqty.php +++ b/app/code/Magento/CatalogInventory/Block/Stockqty/AbstractStockqty.php @@ -131,7 +131,8 @@ public function getPlaceholderId() */ public function isMsgVisible() { - return $this->getStockQty() > 0 && $this->getStockQtyLeft() <= $this->getThresholdQty(); + return $this->getStockQty() > 0 && $this->getStockQtyLeft() > 0 + && $this->getStockQtyLeft() <= $this->getThresholdQty(); } /** diff --git a/app/code/Magento/CatalogInventory/Model/Stock/StockItemRepository.php b/app/code/Magento/CatalogInventory/Model/Stock/StockItemRepository.php index 0d0d42009315e..ee613ac1db018 100644 --- a/app/code/Magento/CatalogInventory/Model/Stock/StockItemRepository.php +++ b/app/code/Magento/CatalogInventory/Model/Stock/StockItemRepository.php @@ -161,7 +161,10 @@ public function save(StockItemInterface $stockItem) $typeId = $product->getTypeId() ?: $product->getTypeInstance()->getTypeId(); $isQty = $this->stockConfiguration->isQty($typeId); if ($isQty) { - $this->changeIsInStockIfNecessary($stockItem); + $isInStock = $this->stockStateProvider->verifyStock($stockItem); + if ($stockItem->getManageStock() && !$isInStock) { + $stockItem->setIsInStock(false)->setStockStatusChangedAutomaticallyFlag(true); + } // if qty is below notify qty, update the low stock date to today date otherwise set null $stockItem->setLowStockDate(null); if ($this->stockStateProvider->verifyNotification($stockItem)) { @@ -257,29 +260,4 @@ private function getStockRegistryStorage() } return $this->stockRegistryStorage; } - - /** - * Change is_in_stock value if necessary. - * - * @param StockItemInterface $stockItem - * - * @return void - */ - private function changeIsInStockIfNecessary(StockItemInterface $stockItem) - { - $isInStock = $this->stockStateProvider->verifyStock($stockItem); - if ($stockItem->getManageStock() && !$isInStock) { - $stockItem->setIsInStock(false)->setStockStatusChangedAutomaticallyFlag(true); - } - - if ($stockItem->getManageStock() - && $isInStock - && !$stockItem->getIsInStock() - && $stockItem->getQty() > 0 - && $stockItem->getOrigData(\Magento\CatalogInventory\Api\Data\StockItemInterface::QTY) <= 0 - && $stockItem->getOrigData(\Magento\CatalogInventory\Api\Data\StockItemInterface::QTY) !== null - ) { - $stockItem->setIsInStock(true)->setStockStatusChangedAutomaticallyFlag(true); - } - } } diff --git a/app/code/Magento/CatalogInventory/Model/StockRegistry.php b/app/code/Magento/CatalogInventory/Model/StockRegistry.php index d688132fdb916..30b08ee4b8e7f 100644 --- a/app/code/Magento/CatalogInventory/Model/StockRegistry.php +++ b/app/code/Magento/CatalogInventory/Model/StockRegistry.php @@ -171,6 +171,16 @@ public function updateStockItemBySku($productSku, \Magento\CatalogInventory\Api\ $productId = $this->resolveProductId($productSku); $websiteId = $stockItem->getWebsiteId() ?: null; $origStockItem = $this->getStockItem($productId, $websiteId); + + if ($stockItem->getManageStock() + && !$stockItem->getIsInStock() + && $stockItem->getQty() > 0 + && $stockItem->getOrigData(\Magento\CatalogInventory\Api\Data\StockItemInterface::QTY) <= 0 + && $stockItem->getOrigData(\Magento\CatalogInventory\Api\Data\StockItemInterface::QTY) !== null + ) { + $stockItem->setIsInStock(true)->setStockStatusChangedAutomaticallyFlag(true); + } + $data = $stockItem->getData(); if ($origStockItem->getItemId()) { unset($data['item_id']); diff --git a/app/code/Magento/CatalogInventory/Observer/CancelOrderItemObserver.php b/app/code/Magento/CatalogInventory/Observer/CancelOrderItemObserver.php index 1e99794d68a40..098e254d785a5 100644 --- a/app/code/Magento/CatalogInventory/Observer/CancelOrderItemObserver.php +++ b/app/code/Magento/CatalogInventory/Observer/CancelOrderItemObserver.php @@ -6,15 +6,21 @@ namespace Magento\CatalogInventory\Observer; -use Magento\Framework\Event\ObserverInterface; use Magento\CatalogInventory\Api\StockManagementInterface; +use Magento\CatalogInventory\Model\Configuration; use Magento\Framework\Event\Observer as EventObserver; +use Magento\Framework\Event\ObserverInterface; /** * Catalog inventory module observer */ class CancelOrderItemObserver implements ObserverInterface { + /** + * @var \Magento\CatalogInventory\Model\Configuration + */ + protected $configuration; + /** * @var StockManagementInterface */ @@ -26,13 +32,16 @@ class CancelOrderItemObserver implements ObserverInterface protected $priceIndexer; /** + * @param Configuration $configuration * @param StockManagementInterface $stockManagement * @param \Magento\Catalog\Model\Indexer\Product\Price\Processor $priceIndexer */ public function __construct( + Configuration $configuration, StockManagementInterface $stockManagement, \Magento\Catalog\Model\Indexer\Product\Price\Processor $priceIndexer ) { + $this->configuration = $configuration; $this->stockManagement = $stockManagement; $this->priceIndexer = $priceIndexer; } @@ -49,7 +58,8 @@ public function execute(EventObserver $observer) $item = $observer->getEvent()->getItem(); $children = $item->getChildrenItems(); $qty = $item->getQtyOrdered() - max($item->getQtyShipped(), $item->getQtyInvoiced()) - $item->getQtyCanceled(); - if ($item->getId() && $item->getProductId() && empty($children) && $qty) { + if ($item->getId() && $item->getProductId() && empty($children) && $qty && $this->configuration + ->getCanBackInStock()) { $this->stockManagement->backItemQty($item->getProductId(), $qty, $item->getStore()->getWebsiteId()); } $this->priceIndexer->reindexRow($item->getProductId()); diff --git a/app/code/Magento/CatalogInventory/Test/Mftf/Data/ProductStockOptionsData.xml b/app/code/Magento/CatalogInventory/Test/Mftf/Data/ProductStockOptionsData.xml new file mode 100644 index 0000000000000..4ff43f4177401 --- /dev/null +++ b/app/code/Magento/CatalogInventory/Test/Mftf/Data/ProductStockOptionsData.xml @@ -0,0 +1,25 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<entities xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:DataGenerator/etc/dataProfileSchema.xsd"> + <!-- Change Maximum Qty Allowed in Shopping Cart config --> + <entity name="ProductStockOptions" type="catalog_inventory_product_stock_options"> + <requiredEntity type="max_qty_to_cart">MaxQtyAllowInCartChange</requiredEntity> + </entity> + <entity name="MaxQtyAllowInCartChange" type="max_qty_to_cart"> + <data key="value">0</data> + </entity> + <!-- Maximum Qty Allowed in Shopping Cart to default config --> + <entity name="DefaultProductStockOptions" type="catalog_inventory_product_stock_options"> + <requiredEntity type="max_qty_to_cart">MaxQtyAllowInCartDefault</requiredEntity> + </entity> + <entity name="MaxQtyAllowInCartDefault" type="max_qty_to_cart"> + <data key="value">10000</data> + </entity> +</entities> diff --git a/app/code/Magento/CatalogInventory/Test/Mftf/Metadata/product_stock_options-meta.xml b/app/code/Magento/CatalogInventory/Test/Mftf/Metadata/product_stock_options-meta.xml new file mode 100644 index 0000000000000..71b1ebd9806ca --- /dev/null +++ b/app/code/Magento/CatalogInventory/Test/Mftf/Metadata/product_stock_options-meta.xml @@ -0,0 +1,21 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<operations xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:mftf:DataGenerator/etc/dataOperation.xsd"> + <operation name="CatalogInventoryProductStockSetup" dataType="catalog_inventory_product_stock_options" type="create" auth="adminFormKey" url="/admin/system_config/save/section/cataloginventory/" successRegex="/messages-message-success/" method="POST"> + <object key="groups" dataType="catalog_inventory_product_stock_options"> + <object key="item_options" dataType="catalog_inventory_product_stock_options"> + <object key="fields" dataType="catalog_inventory_product_stock_options"> + <object key="max_sale_qty" dataType="max_qty_to_cart"> + <field key="value">string</field> + </object> + </object> + </object> + </object> + </operation> +</operations> diff --git a/app/code/Magento/CatalogInventory/Test/Unit/Model/Stock/StockItemRepositoryTest.php b/app/code/Magento/CatalogInventory/Test/Unit/Model/Stock/StockItemRepositoryTest.php index 6b1770ff7d403..293874bb32b9f 100644 --- a/app/code/Magento/CatalogInventory/Test/Unit/Model/Stock/StockItemRepositoryTest.php +++ b/app/code/Magento/CatalogInventory/Test/Unit/Model/Stock/StockItemRepositoryTest.php @@ -276,7 +276,7 @@ public function testSave() ->method('verifyStock') ->with($this->stockItemMock) ->willReturn(false); - $this->stockItemMock->expects($this->exactly(2))->method('getManageStock')->willReturn(true); + $this->stockItemMock->expects($this->once())->method('getManageStock')->willReturn(true); $this->stockItemMock->expects($this->once())->method('setIsInStock')->with(false)->willReturnSelf(); $this->stockItemMock->expects($this->once()) ->method('setStockStatusChangedAutomaticallyFlag') diff --git a/app/code/Magento/CatalogInventory/Test/Unit/Model/StockRegistryTest.php b/app/code/Magento/CatalogInventory/Test/Unit/Model/StockRegistryTest.php index c04bd1e9e4402..03e60a9d39d5e 100644 --- a/app/code/Magento/CatalogInventory/Test/Unit/Model/StockRegistryTest.php +++ b/app/code/Magento/CatalogInventory/Test/Unit/Model/StockRegistryTest.php @@ -5,6 +5,8 @@ */ namespace Magento\CatalogInventory\Test\Unit\Model; +use PHPUnit_Framework_MockObject_MockObject as MockObject; + /** * Class StockRegistryTest */ @@ -16,37 +18,188 @@ class StockRegistryTest extends \PHPUnit\Framework\TestCase protected $model; /** - * @var \PHPUnit_Framework_MockObject_MockObject + * @var \Magento\CatalogInventory\Api\StockItemCriteriaInterface|MockObject */ protected $criteria; + /** + * @var \Magento\CatalogInventory\Api\StockItemCriteriaInterfaceFactory|MockObject + */ + private $criteriaFactory; + + /** + * @var \Magento\CatalogInventory\Api\Data\StockItemInterface|MockObject + */ + private $stockItemMock; + + /** + * @var \Magento\CatalogInventory\Api\StockConfigurationInterface|MockObject + */ + private $stockConfigurationMock; + + /** + * @var \Magento\CatalogInventory\Api\StockItemRepositoryInterface|MockObject + */ + private $stockItemRepositoryMock; + + /** + * @var \Magento\CatalogInventory\Model\Spi\StockRegistryProviderInterface|MockObject + */ + private $stockRegistryProviderMock; + + /** + * @var \Magento\Catalog\Model\ProductFactory|MockObject + */ + private $productFactoryMock; + protected function setUp() { $this->criteria = $this->getMockBuilder(\Magento\CatalogInventory\Api\StockItemCriteriaInterface::class) ->disableOriginalConstructor() ->getMock(); - $criteriaFactory = $this->getMockBuilder(\Magento\CatalogInventory\Api\StockItemCriteriaInterfaceFactory::class) - ->setMethods(['create']) + $this->criteriaFactory = $this->getMockBuilder( + \Magento\CatalogInventory\Api\StockItemCriteriaInterfaceFactory::class + )->setMethods(['create']) ->disableOriginalConstructor() ->getMock(); - $criteriaFactory->expects($this->once())->method('create')->willReturn($this->criteria); + + $this->stockItemMock = $this->getMockBuilder(\Magento\CatalogInventory\Api\Data\StockItemInterface::class) + ->disableOriginalConstructor() + ->setMethods([ + 'getWebsiteId', + 'getData', + 'addData', + 'getManageStock', + 'getIsInStock', + 'getQty', + 'getOrigData', + 'setIsInStock', + 'setStockStatusChangedAutomaticallyFlag', + ]) + ->getMockForAbstractClass(); + + $this->stockConfigurationMock = $this->createMock( + \Magento\CatalogInventory\Api\StockConfigurationInterface::class + ); + $this->stockRegistryProviderMock = $this->createMock( + \Magento\CatalogInventory\Model\Spi\StockRegistryProviderInterface::class + ); + $this->stockItemRepositoryMock = $this->createMock( + \Magento\CatalogInventory\Api\StockItemRepositoryInterface::class + ); + $this->productFactoryMock = $this->createPartialMock(\Magento\Catalog\Model\ProductFactory::class, ['create']); $objectManager = new \Magento\Framework\TestFramework\Unit\Helper\ObjectManager($this); $this->model = $objectManager->getObject( \Magento\CatalogInventory\Model\StockRegistry::class, [ - 'criteriaFactory' => $criteriaFactory + 'stockConfiguration' => $this->stockConfigurationMock, + 'stockRegistryProvider' => $this->stockRegistryProviderMock, + 'stockItemRepository' => $this->stockItemRepositoryMock, + 'criteriaFactory' => $this->criteriaFactory, + 'productFactory' => $this->productFactoryMock, ] ); } public function testGetLowStockItems() { + $this->criteriaFactory->expects($this->once())->method('create')->willReturn($this->criteria); $this->criteria->expects($this->once())->method('setLimit')->with(1, 0); $this->criteria->expects($this->once())->method('setScopeFilter')->with(1); $this->criteria->expects($this->once())->method('setQtyFilter')->with('<='); $this->criteria->expects($this->once())->method('addField')->with('qty'); $this->model->getLowStockItems(1, 100); } + + /** + * @return void + */ + public function testUpdateStockItemBySku() + { + $manageStock = 1; + $isInStock = 0; + $qty = 10; + $origQty = 0; + + $this->stockItemMock->expects($this->once())->method('getManageStock')->willReturn($manageStock); + $this->stockItemMock->expects($this->once())->method('getIsInStock')->willReturn($isInStock); + $this->stockItemMock->expects($this->once())->method('getQty')->willReturn($qty); + $this->stockItemMock->expects($this->exactly(2)) + ->method('getOrigData') + ->with(\Magento\CatalogInventory\Api\Data\StockItemInterface::QTY) + ->willReturn($origQty); + + $this->stockItemMock->expects($this->once())->method('setIsInStock')->with(true)->willReturnSelf(); + $this->stockItemMock->expects($this->once()) + ->method('setStockStatusChangedAutomaticallyFlag') + ->with(true) + ->willReturnSelf(); + + $this->configureAndCallUpdateStockItemBySku(); + } + + /** + * @return void + */ + public function testUpdateStockItemBySkuWithoutUpdateStockStatus() + { + $manageStock = 0; + + $this->stockItemMock->expects($this->once())->method('getManageStock')->willReturn($manageStock); + $this->stockItemMock->expects($this->never())->method('getIsInStock'); + $this->stockItemMock->expects($this->never())->method('getQty'); + $this->stockItemMock->expects($this->never()) + ->method('getOrigData') + ->with(\Magento\CatalogInventory\Api\Data\StockItemInterface::QTY); + + $this->stockItemMock->expects($this->never())->method('setIsInStock')->with(true); + $this->stockItemMock->expects($this->never())->method('setStockStatusChangedAutomaticallyFlag')->with(true); + + $this->configureAndCallUpdateStockItemBySku(); + } + + /** + * @return void + */ + private function configureAndCallUpdateStockItemBySku() + { + $productId = 1; + $productSku = 'Simple'; + $websiteId = 0; + $scopeId = 1; + $data = ['item_id' => 1, 'is_in_stock' => 1]; + + /** @var \Magento\CatalogInventory\Api\Data\StockItemInterface|MockObject $origStockItemMock */ + $origStockItemMock = $this->getMockBuilder(\Magento\CatalogInventory\Api\Data\StockItemInterface::class) + ->disableOriginalConstructor() + ->setMethods(['getItemId', 'addData', 'setProductId']) + ->getMockForAbstractClass(); + + /** @var \Magento\Catalog\Model\Product|MockObject $productMock */ + $productMock = $this->createPartialMock(\Magento\Catalog\Model\Product::class, ['getIdBySku']); + $this->productFactoryMock->expects($this->once())->method('create')->willReturn($productMock); + $productMock->expects($this->once())->method('getIdBySku')->with($productSku)->willReturn($productId); + + $this->stockItemMock->expects($this->once())->method('getWebsiteId')->willReturn($websiteId); + $this->stockItemMock->expects($this->once())->method('getData')->willReturn($data); + + $origStockItemMock->expects($this->exactly(2))->method('getItemId')->willReturn(null); + $origStockItemMock->expects($this->once())->method('addData')->with($data)->willReturnSelf(); + $origStockItemMock->expects($this->once())->method('setProductId')->with($productId)->willReturnSelf(); + + $this->stockConfigurationMock->expects($this->once())->method('getDefaultScopeId')->willReturn($scopeId); + $this->stockRegistryProviderMock->expects($this->once()) + ->method('getStockItem') + ->with($productId, $scopeId) + ->willReturn($origStockItemMock); + + $this->stockItemRepositoryMock->expects($this->once()) + ->method('save') + ->with($origStockItemMock) + ->willReturn($origStockItemMock); + + $this->model->updateStockItemBySku($productSku, $this->stockItemMock); + } } diff --git a/app/code/Magento/CatalogRule/Model/Indexer/AbstractIndexer.php b/app/code/Magento/CatalogRule/Model/Indexer/AbstractIndexer.php index 5d93e6f216866..6b7c12dfdf463 100644 --- a/app/code/Magento/CatalogRule/Model/Indexer/AbstractIndexer.php +++ b/app/code/Magento/CatalogRule/Model/Indexer/AbstractIndexer.php @@ -10,6 +10,9 @@ use Magento\Framework\DataObject\IdentityInterface; use Magento\Framework\Indexer\CacheContext; +/** + * Abstract class for CatalogRule indexers. + */ abstract class AbstractIndexer implements IndexerActionInterface, MviewActionInterface, IdentityInterface { /** @@ -66,7 +69,6 @@ public function executeFull() { $this->indexBuilder->reindexFull(); $this->_eventManager->dispatch('clean_cache_by_tags', ['object' => $this]); - //TODO: remove after fix fpc. MAGETWO-50668 $this->getCacheManager()->clean($this->getIdentities()); } @@ -137,8 +139,9 @@ public function executeRow($id) abstract protected function doExecuteRow($id); /** - * @return \Magento\Framework\App\CacheInterface|mixed + * Get cache manager * + * @return \Magento\Framework\App\CacheInterface|mixed * @deprecated 100.0.7 */ private function getCacheManager() diff --git a/app/code/Magento/CatalogRule/Test/Mftf/ActionGroup/CatalogPriceRuleActionGroup.xml b/app/code/Magento/CatalogRule/Test/Mftf/ActionGroup/CatalogPriceRuleActionGroup.xml index 72e082392059e..51c87831d1f71 100644 --- a/app/code/Magento/CatalogRule/Test/Mftf/ActionGroup/CatalogPriceRuleActionGroup.xml +++ b/app/code/Magento/CatalogRule/Test/Mftf/ActionGroup/CatalogPriceRuleActionGroup.xml @@ -68,4 +68,30 @@ <fillField selector="{{AdminCatalogPriceRuleConditionsSection.targetInput('1', '1')}}" userInput="{{productSku}}" after="clickEllipsis" stepKey="fillProductSku"/> <click selector="{{AdminCatalogPriceRuleConditionsSection.applyButton('1', '1')}}" after="fillProductSku" stepKey="clickApply"/> </actionGroup> + <!--Add Catalog Rule Condition With Category--> + <actionGroup name="newCatalogPriceRuleByUIWithConditionIsCategory" extends="CreateCatalogPriceRule"> + <arguments> + <argument name="categoryId" type="string"/> + </arguments> + <click selector="{{AdminCatalogPriceRuleSection.conditionsTab}}" after="discardSubsequentRules" stepKey="openConditionsTab"/> + <click selector="{{AdminCatalogPriceRuleConditionsSection.newCondition}}" after="openConditionsTab" stepKey="addNewCondition"/> + <selectOption selector="{{AdminCatalogPriceRuleConditionsSection.conditionSelect('1')}}" userInput="Magento\CatalogRule\Model\Rule\Condition\Product|category_ids" after="addNewCondition" stepKey="selectTypeCondition"/> + <click selector="{{AdminCatalogPriceRuleConditionsSection.targetEllipsis('1')}}" after="selectTypeCondition" stepKey="clickEllipsis"/> + <fillField selector="{{AdminCatalogPriceRuleConditionsSection.targetInput('1', '1')}}" userInput="{{categoryId}}" after="clickEllipsis" stepKey="fillCategoryId"/> + <click selector="{{AdminCatalogPriceRuleConditionsSection.applyButton('1', '1')}}" after="fillCategoryId" stepKey="clickApply"/> + </actionGroup> + + <!-- Open rule for Edit --> + <actionGroup name="OpenCatalogPriceRule"> + <arguments> + <argument name="ruleName" type="string" defaultValue="CustomCatalogRule.name"/> + </arguments> + + <amOnPage url="{{AdminCatalogPriceRuleGridPage.url}}" stepKey="goToAdminCatalogPriceRuleGridPage"/> + <conditionalClick selector="{{AdminDataGridHeaderSection.clearFilters}}" dependentSelector="{{AdminDataGridHeaderSection.clearFilters}}" visible="true" stepKey="clearExistingFilters"/> + <fillField selector="{{AdminCatalogPriceRuleGridSection.filterByRuleName}}" userInput="{{ruleName}}" stepKey="filterByRuleName"/> + <click selector="{{AdminDataGridHeaderSection.applyFilters}}" stepKey="clickSearch"/> + <click selector="{{AdminGridTableSection.row('1')}}" stepKey="clickEdit"/> + <waitForPageLoad stepKey="waitForPageLoad"/> + </actionGroup> </actionGroups> diff --git a/app/code/Magento/CatalogRule/Test/Mftf/Test/StorefrontCatalogPriceRuleAndCustomerGroupMembershipArePersistedUnderLongTermCookieTest.xml b/app/code/Magento/CatalogRule/Test/Mftf/Test/StorefrontCatalogPriceRuleAndCustomerGroupMembershipArePersistedUnderLongTermCookieTest.xml new file mode 100644 index 0000000000000..94413788bdc21 --- /dev/null +++ b/app/code/Magento/CatalogRule/Test/Mftf/Test/StorefrontCatalogPriceRuleAndCustomerGroupMembershipArePersistedUnderLongTermCookieTest.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="StorefrontCatalogPriceRuleAndCustomerGroupMembershipArePersistedUnderLongTermCookieTest"> + <annotations> + <features value="Persistent"/> + <stories value="Check the price"/> + <title value="Verify that Catalog Price Rule and Customer Group Membership are persisted under long-term cookie"/> + <description value="Verify that Catalog Price Rule and Customer Group Membership are persisted under long-term cookie"/> + <severity value="CRITICAL"/> + <testCaseId value="MAGETWO-76169"/> + <group value="persistent"/> + </annotations> + <before> + <createData entity="PersistentConfigEnabled" stepKey="enablePersistent"/> + <createData entity="PersistentLogoutClearDisabled" stepKey="persistentLogoutClearDisable"/> + <createData entity="_defaultCategory" stepKey="createCategory"/> + <createData entity="_defaultProduct" stepKey="createProduct"> + <requiredEntity createDataKey="createCategory"/> + <field key="price">50</field> + </createData> + <createData entity="Simple_US_Customer" stepKey="createCustomer"> + <field key="group_id">1</field> + </createData> + + <actionGroup ref="LoginAsAdmin" stepKey="login"/> + <!--Create Catalog Rule--> + <actionGroup ref="newCatalogPriceRuleByUIWithConditionIsCategory" stepKey="createCatalogPriceRule"> + <argument name="catalogRule" value="CustomCatalogRule"/> + <argument name="categoryId" value="$$createCategory.id$$"/> + </actionGroup> + <click selector="{{AdminCatalogPriceRuleGridSection.applyRulesButton}}" stepKey="clickApplyRules"/> + + <magentoCLI command="indexer:reindex" stepKey="reindex"/> + <magentoCLI command="cache:flush" stepKey="flushCache"/> + </before> + <after> + <!-- Delete the rule --> + <actionGroup ref="RemoveCatalogPriceRule" stepKey="deletePriceRule"> + <argument name="ruleName" value="CustomCatalogRule.name" /> + </actionGroup> + <actionGroup ref="logout" stepKey="logout"/> + <createData entity="PersistentConfigDefault" stepKey="setDefaultPersistentState"/> + <createData entity="PersistentLogoutClearEnabled" stepKey="persistentLogoutClearEnabled"/> + <deleteData createDataKey="createCategory" stepKey="deleteCategory"/> + <deleteData createDataKey="createProduct" stepKey="deleteProduct"/> + <deleteData createDataKey="createCustomer" stepKey="deleteCustomer"/> + </after> + + <!--Go to category and check price--> + <amOnPage url="{{StorefrontCategoryPage.url($$createCategory.name$$)}}" stepKey="onStorefrontCategoryPage"/> + <see selector="{{StorefrontCategoryProductSection.ProductPriceByName($$createProduct.name$$)}}" userInput="$$createProduct.price$$" stepKey="checkPriceSimpleProduct"/> + + <!--Login to storfront from customer and check price--> + <actionGroup ref="CustomerLoginOnStorefront" stepKey="logInFromCustomer"> + <argument name="customer" value="$$createCustomer$$"/> + </actionGroup> + <amOnPage url="{{StorefrontCategoryPage.url($$createCategory.name$$)}}" stepKey="onStorefrontCategoryPage2"/> + <see userInput="Welcome, $$createCustomer.firstname$$ $$createCustomer.lastname$$!" selector="{{StorefrontPanelHeaderSection.welcomeMessage}}" stepKey="homeCheckWelcome"/> + <see selector="{{StorefrontCategoryProductSection.ProductPriceByName($$createProduct.name$$)}}" userInput="45.00" stepKey="checkPriceSimpleProduct2"/> + + <!--Click *Sign Out* and check the price of the Simple Product--> + <actionGroup ref="CustomerLogoutStorefrontActionGroup" stepKey="storefrontSignOut"/> + <amOnPage url="{{StorefrontCategoryPage.url($$createCategory.name$$)}}" stepKey="onStorefrontCategoryPage3"/> + <see userInput="Welcome, $$createCustomer.firstname$$ $$createCustomer.lastname$$!" selector="{{StorefrontPanelHeaderSection.welcomeMessage}}" stepKey="homeCheckWelcome2"/> + <seeElement selector="{{StorefrontPanelHeaderSection.notYouLink}}" stepKey="checkLinkNotYou"/> + <see selector="{{StorefrontCategoryProductSection.ProductPriceByName($$createProduct.name$$)}}" userInput="45.00" stepKey="checkPriceSimpleProduct3"/> + + <!--Click the *Not you?* link and check the price for Simple Product--> + <click selector="{{StorefrontPanelHeaderSection.notYouLink}}" stepKey="clickNext"/> + <amOnPage url="{{StorefrontCategoryPage.url($$createCategory.name$$)}}" stepKey="onStorefrontCategoryPage4"/> + <see userInput="Default welcome msg!" selector="{{StorefrontPanelHeaderSection.welcomeMessage}}" stepKey="homeCheckWelcome3"/> + <see selector="{{StorefrontCategoryProductSection.ProductPriceByName($$createProduct.name$$)}}" userInput="$$createProduct.price$$" stepKey="checkPriceSimpleProduct4"/> + </test> +</tests> 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 286b6427c8132..7a94dcb78cfd5 100644 --- a/app/code/Magento/CatalogSearch/Model/Indexer/Fulltext/Action/DataProvider.php +++ b/app/code/Magento/CatalogSearch/Model/Indexer/Fulltext/Action/DataProvider.php @@ -12,6 +12,8 @@ use Magento\Store\Model\Store; /** + * Catalog search full text search data provider. + * * @SuppressWarnings(PHPMD.CouplingBetweenObjects) * @SuppressWarnings(PHPMD.TooManyFields) * @api @@ -571,8 +573,8 @@ public function prepareProductIndex($indexData, $productData, $storeId) } } foreach ($indexData as $entityId => $attributeData) { - foreach ($attributeData as $attributeId => $attributeValue) { - $value = $this->getAttributeValue($attributeId, $attributeValue, $storeId); + foreach ($attributeData as $attributeId => $attributeValues) { + $value = $this->getAttributeValue($attributeId, $attributeValues, $storeId); if (!empty($value)) { if (isset($index[$attributeId])) { $index[$attributeId][$entityId] = $value; @@ -603,16 +605,16 @@ public function prepareProductIndex($indexData, $productData, $storeId) * Retrieve attribute source value for search * * @param int $attributeId - * @param mixed $valueId + * @param mixed $valueIds * @param int $storeId * @return string */ - private function getAttributeValue($attributeId, $valueId, $storeId) + private function getAttributeValue($attributeId, $valueIds, $storeId) { $attribute = $this->getSearchableAttribute($attributeId); - $value = $this->engine->processAttributeValue($attribute, $valueId); + $value = $this->engine->processAttributeValue($attribute, $valueIds); if (false !== $value) { - $optionValue = $this->getAttributeOptionValue($attributeId, $valueId, $storeId); + $optionValue = $this->getAttributeOptionValue($attributeId, $valueIds, $storeId); if (null === $optionValue) { $value = preg_replace('/\s+/iu', ' ', trim(strip_tags($value))); } else { @@ -627,13 +629,15 @@ private function getAttributeValue($attributeId, $valueId, $storeId) * Get attribute option value * * @param int $attributeId - * @param int $valueId + * @param int|string $valueIds * @param int $storeId * @return null|string */ - private function getAttributeOptionValue($attributeId, $valueId, $storeId) + private function getAttributeOptionValue($attributeId, $valueIds, $storeId) { $optionKey = $attributeId . '-' . $storeId; + $attributeValueIds = explode(',', $valueIds); + $attributeOptionValue = ''; if (!array_key_exists($optionKey, $this->attributeOptions) ) { $attribute = $this->getSearchableAttribute($attributeId); @@ -649,6 +653,12 @@ private function getAttributeOptionValue($attributeId, $valueId, $storeId) } } - return $this->attributeOptions[$optionKey][$valueId] ?? null; + foreach ($attributeValueIds as $attributeValueId) { + if (isset($this->attributeOptions[$optionKey][$attributeValueId])) { + $attributeOptionValue .= $this->attributeOptions[$optionKey][$attributeValueId] . ' '; + } + } + + return empty($attributeOptionValue) ? null : trim($attributeOptionValue); } } diff --git a/app/code/Magento/CatalogSearch/Model/Layer/Filter/Attribute.php b/app/code/Magento/CatalogSearch/Model/Layer/Filter/Attribute.php index 7aac6e98fc044..7b239d84bf962 100644 --- a/app/code/Magento/CatalogSearch/Model/Layer/Filter/Attribute.php +++ b/app/code/Magento/CatalogSearch/Model/Layer/Filter/Attribute.php @@ -91,12 +91,10 @@ protected function _getItemsData() return $this->itemDataBuilder->build(); } - $productSize = $productCollection->getSize(); - $options = $attribute->getFrontend() ->getSelectOptions(); foreach ($options as $option) { - $this->buildOptionData($option, $isAttributeFilterable, $optionsFacetedData, $productSize); + $this->buildOptionData($option, $isAttributeFilterable, $optionsFacetedData); } return $this->itemDataBuilder->build(); @@ -108,17 +106,16 @@ protected function _getItemsData() * @param array $option * @param boolean $isAttributeFilterable * @param array $optionsFacetedData - * @param int $productSize * @return void */ - private function buildOptionData($option, $isAttributeFilterable, $optionsFacetedData, $productSize) + private function buildOptionData($option, $isAttributeFilterable, $optionsFacetedData) { $value = $this->getOptionValue($option); if ($value === false) { return; } $count = $this->getOptionCount($value, $optionsFacetedData); - if ($isAttributeFilterable && (!$this->isOptionReducesResults($count, $productSize) || $count === 0)) { + if ($isAttributeFilterable && $count === 0) { return; } diff --git a/app/code/Magento/CatalogSearch/Model/ResourceModel/Engine.php b/app/code/Magento/CatalogSearch/Model/ResourceModel/Engine.php index fac8c4d2a47f6..831780631c124 100644 --- a/app/code/Magento/CatalogSearch/Model/ResourceModel/Engine.php +++ b/app/code/Magento/CatalogSearch/Model/ResourceModel/Engine.php @@ -107,7 +107,10 @@ public function processAttributeValue($attribute, $value) && in_array($attribute->getFrontendInput(), ['text', 'textarea']) ) { $result = $value; - } elseif ($this->isTermFilterableAttribute($attribute)) { + } elseif ($this->isTermFilterableAttribute($attribute) + || ($attribute->getIsSearchable() + && in_array($attribute->getFrontendInput(), ['select', 'multiselect'])) + ) { $result = ''; } @@ -115,7 +118,8 @@ public function processAttributeValue($attribute, $value) } /** - * Prepare index array as a string glued by separator + * Prepare index array as a string glued by separator. + * * Support 2 level array gluing * * @param array $index diff --git a/app/code/Magento/CatalogSearch/Test/Unit/Model/Layer/Filter/AttributeTest.php b/app/code/Magento/CatalogSearch/Test/Unit/Model/Layer/Filter/AttributeTest.php index abc0fdd1069fe..69e2c33d02d1a 100644 --- a/app/code/Magento/CatalogSearch/Test/Unit/Model/Layer/Filter/AttributeTest.php +++ b/app/code/Magento/CatalogSearch/Test/Unit/Model/Layer/Filter/AttributeTest.php @@ -321,10 +321,6 @@ public function testGetItemsWithoutApply() ->method('build') ->will($this->returnValue($builtData)); - $this->fulltextCollection->expects($this->once()) - ->method('getSize') - ->will($this->returnValue(50)); - $expectedFilterItems = [ $this->createFilterItem(0, $builtData[0]['label'], $builtData[0]['value'], $builtData[0]['count']), $this->createFilterItem(1, $builtData[1]['label'], $builtData[1]['value'], $builtData[1]['count']), @@ -383,9 +379,6 @@ public function testGetItemsOnlyWithResults() $this->fulltextCollection->expects($this->once()) ->method('getFacetedData') ->willReturn($facetedData); - $this->fulltextCollection->expects($this->once()) - ->method('getSize') - ->will($this->returnValue(50)); $this->itemDataBuilder->expects($this->once()) ->method('addItemData') diff --git a/app/code/Magento/CatalogSearch/etc/indexer.xml b/app/code/Magento/CatalogSearch/etc/indexer.xml index 9726f5372311d..a0b9ca10afccb 100644 --- a/app/code/Magento/CatalogSearch/etc/indexer.xml +++ b/app/code/Magento/CatalogSearch/etc/indexer.xml @@ -9,8 +9,6 @@ <indexer id="catalogsearch_fulltext" view_id="catalogsearch_fulltext" class="Magento\CatalogSearch\Model\Indexer\Fulltext"> <title translate="true">Catalog Search Rebuild Catalog product fulltext search index - - diff --git a/app/code/Magento/CatalogUrlRewrite/Model/Category/ChildrenUrlRewriteGenerator.php b/app/code/Magento/CatalogUrlRewrite/Model/Category/ChildrenUrlRewriteGenerator.php index 6aa33f37cd31f..beed1e18582c1 100644 --- a/app/code/Magento/CatalogUrlRewrite/Model/Category/ChildrenUrlRewriteGenerator.php +++ b/app/code/Magento/CatalogUrlRewrite/Model/Category/ChildrenUrlRewriteGenerator.php @@ -5,12 +5,16 @@ */ namespace Magento\CatalogUrlRewrite\Model\Category; +use Magento\Catalog\Api\CategoryRepositoryInterface; use Magento\Catalog\Model\Category; -use Magento\CatalogUrlRewrite\Model\CategoryUrlRewriteGeneratorFactory; use Magento\CatalogUrlRewrite\Model\CategoryUrlRewriteGenerator; -use Magento\UrlRewrite\Model\MergeDataProviderFactory; +use Magento\CatalogUrlRewrite\Model\CategoryUrlRewriteGeneratorFactory; use Magento\Framework\App\ObjectManager; +use Magento\UrlRewrite\Model\MergeDataProviderFactory; +/** + * Model for generate url rewrites for children categories. + */ class ChildrenUrlRewriteGenerator { /** @@ -28,15 +32,22 @@ class ChildrenUrlRewriteGenerator */ private $mergeDataProviderPrototype; + /** + * @var CategoryRepositoryInterface + */ + private $categoryRepository; + /** * @param \Magento\CatalogUrlRewrite\Model\Category\ChildrenCategoriesProvider $childrenCategoriesProvider * @param \Magento\CatalogUrlRewrite\Model\CategoryUrlRewriteGeneratorFactory $categoryUrlRewriteGeneratorFactory * @param \Magento\UrlRewrite\Model\MergeDataProviderFactory|null $mergeDataProviderFactory + * @param CategoryRepositoryInterface|null $categoryRepository */ public function __construct( ChildrenCategoriesProvider $childrenCategoriesProvider, CategoryUrlRewriteGeneratorFactory $categoryUrlRewriteGeneratorFactory, - MergeDataProviderFactory $mergeDataProviderFactory = null + MergeDataProviderFactory $mergeDataProviderFactory = null, + CategoryRepositoryInterface $categoryRepository = null ) { $this->childrenCategoriesProvider = $childrenCategoriesProvider; $this->categoryUrlRewriteGeneratorFactory = $categoryUrlRewriteGeneratorFactory; @@ -44,6 +55,8 @@ public function __construct( $mergeDataProviderFactory = ObjectManager::getInstance()->get(MergeDataProviderFactory::class); } $this->mergeDataProviderPrototype = $mergeDataProviderFactory->create(); + $this->categoryRepository = $categoryRepository + ?: ObjectManager::getInstance()->get(CategoryRepositoryInterface::class); } /** @@ -57,14 +70,18 @@ public function __construct( public function generate($storeId, Category $category, $rootCategoryId = null) { $mergeDataProvider = clone $this->mergeDataProviderPrototype; - foreach ($this->childrenCategoriesProvider->getChildren($category, true) as $childCategory) { - $childCategory->setStoreId($storeId); - $childCategory->setData('save_rewrites_history', $category->getData('save_rewrites_history')); - /** @var CategoryUrlRewriteGenerator $categoryUrlRewriteGenerator */ - $categoryUrlRewriteGenerator = $this->categoryUrlRewriteGeneratorFactory->create(); - $mergeDataProvider->merge( - $categoryUrlRewriteGenerator->generate($childCategory, false, $rootCategoryId) - ); + $childrenIds = $this->childrenCategoriesProvider->getChildrenIds($category, true); + if ($childrenIds) { + foreach ($childrenIds as $childId) { + /** @var Category $childCategory */ + $childCategory = $this->categoryRepository->get($childId, $storeId); + $childCategory->setData('save_rewrites_history', $category->getData('save_rewrites_history')); + /** @var CategoryUrlRewriteGenerator $categoryUrlRewriteGenerator */ + $categoryUrlRewriteGenerator = $this->categoryUrlRewriteGeneratorFactory->create(); + $mergeDataProvider->merge( + $categoryUrlRewriteGenerator->generate($childCategory, false, $rootCategoryId) + ); + } } return $mergeDataProvider->getData(); diff --git a/app/code/Magento/CatalogUrlRewrite/Model/CategoryUrlPathGenerator.php b/app/code/Magento/CatalogUrlRewrite/Model/CategoryUrlPathGenerator.php index ee20b0e934b5d..2961dc4358970 100644 --- a/app/code/Magento/CatalogUrlRewrite/Model/CategoryUrlPathGenerator.php +++ b/app/code/Magento/CatalogUrlRewrite/Model/CategoryUrlPathGenerator.php @@ -8,6 +8,9 @@ use Magento\Catalog\Api\CategoryRepositoryInterface; use Magento\Catalog\Model\Category; +/** + * Class for generation category url_path. + */ class CategoryUrlPathGenerator { /** @@ -58,12 +61,13 @@ public function __construct( } /** - * Build category URL path + * Build category URL path. * * @param \Magento\Catalog\Api\Data\CategoryInterface|\Magento\Framework\Model\AbstractModel $category + * @param null|\Magento\Catalog\Api\Data\CategoryInterface|\Magento\Framework\Model\AbstractModel $parentCategory * @return string */ - public function getUrlPath($category) + public function getUrlPath($category, $parentCategory = null) { if (in_array($category->getParentId(), [Category::ROOT_CATEGORY_ID, Category::TREE_ROOT_ID])) { return ''; @@ -77,15 +81,18 @@ public function getUrlPath($category) return $category->getUrlPath(); } if ($this->isNeedToGenerateUrlPathForParent($category)) { - $parentPath = $this->getUrlPath( - $this->categoryRepository->get($category->getParentId(), $category->getStoreId()) - ); + $parentCategory = $parentCategory ?? + $this->categoryRepository->get($category->getParentId(), $category->getStoreId()); + $parentPath = $this->getUrlPath($parentCategory); $path = $parentPath === '' ? $path : $parentPath . '/' . $path; } + return $path; } /** + * Define whether we should generate URL path for parent. + * * @param \Magento\Catalog\Model\Category $category * @return bool */ diff --git a/app/code/Magento/CatalogUrlRewrite/Observer/CategoryProcessUrlRewriteSavingObserver.php b/app/code/Magento/CatalogUrlRewrite/Observer/CategoryProcessUrlRewriteSavingObserver.php index 5130b43333d47..745c302d619a1 100644 --- a/app/code/Magento/CatalogUrlRewrite/Observer/CategoryProcessUrlRewriteSavingObserver.php +++ b/app/code/Magento/CatalogUrlRewrite/Observer/CategoryProcessUrlRewriteSavingObserver.php @@ -100,16 +100,19 @@ public function execute(\Magento\Framework\Event\Observer $observer) } $mapsGenerated = false; - if ($category->dataHasChangedFor('url_key') - || $category->dataHasChangedFor('is_anchor') - || $category->getChangedProductIds() - ) { + if ($this->isCategoryHasChanged($category)) { if ($category->dataHasChangedFor('url_key')) { $categoryUrlRewriteResult = $this->categoryUrlRewriteGenerator->generate($category); $this->urlRewriteBunchReplacer->doBunchReplace($categoryUrlRewriteResult); } - $productUrlRewriteResult = $this->urlRewriteHandler->generateProductUrlRewrites($category); - $this->urlRewriteBunchReplacer->doBunchReplace($productUrlRewriteResult); + if ($this->isChangedOnlyProduct($category)) { + $productUrlRewriteResult = + $this->urlRewriteHandler->updateProductUrlRewritesForChangedProduct($category); + $this->urlRewriteBunchReplacer->doBunchReplace($productUrlRewriteResult); + } else { + $productUrlRewriteResult = $this->urlRewriteHandler->generateProductUrlRewrites($category); + $this->urlRewriteBunchReplacer->doBunchReplace($productUrlRewriteResult); + } $mapsGenerated = true; } @@ -120,8 +123,42 @@ public function execute(\Magento\Framework\Event\Observer $observer) } /** - * in case store_id is not set for category then we can assume that it was passed through product import. - * store group must have only one root category, so receiving category's path and checking if one of it parts + * Check is category changed changed. + * + * @param Category $category + * @return bool + */ + private function isCategoryHasChanged(Category $category): bool + { + if ($category->dataHasChangedFor('url_key') + || $category->dataHasChangedFor('is_anchor') + || !empty($category->getChangedProductIds())) { + return true; + } + + return false; + } + + /** + * Check is only product changed. + * + * @param Category $category + * @return bool + */ + private function isChangedOnlyProduct(Category $category): bool + { + if (!empty($category->getChangedProductIds()) + && !$category->dataHasChangedFor('is_anchor') + && !$category->dataHasChangedFor('url_key')) { + return true; + } + + return false; + } + + /** + * In case store_id is not set for category then we can assume that it was passed through product import. + * Store group must have only one root category, so receiving category's path and checking if one of it parts * is the root category for store group, we can set default_store_id value from it to category. * it prevents urls duplication for different stores * ("Default Category/category/sub" and "Default Category2/category/sub") diff --git a/app/code/Magento/CatalogUrlRewrite/Observer/CategoryUrlPathAutogeneratorObserver.php b/app/code/Magento/CatalogUrlRewrite/Observer/CategoryUrlPathAutogeneratorObserver.php index eb54f0427c11a..b49777e08bda9 100644 --- a/app/code/Magento/CatalogUrlRewrite/Observer/CategoryUrlPathAutogeneratorObserver.php +++ b/app/code/Magento/CatalogUrlRewrite/Observer/CategoryUrlPathAutogeneratorObserver.php @@ -5,14 +5,17 @@ */ namespace Magento\CatalogUrlRewrite\Observer; +use Magento\Catalog\Api\CategoryRepositoryInterface; use Magento\Catalog\Model\Category; +use Magento\CatalogUrlRewrite\Model\Category\ChildrenCategoriesProvider; use Magento\CatalogUrlRewrite\Model\CategoryUrlPathGenerator; use Magento\CatalogUrlRewrite\Service\V1\StoreViewService; -use Magento\Framework\Event\Observer; -use Magento\CatalogUrlRewrite\Model\Category\ChildrenCategoriesProvider; use Magento\Framework\Event\ObserverInterface; use Magento\Store\Model\Store; +/** + * Class observer to initiate generation category url_path. + */ class CategoryUrlPathAutogeneratorObserver implements ObserverInterface { /** @@ -30,22 +33,32 @@ class CategoryUrlPathAutogeneratorObserver implements ObserverInterface */ protected $storeViewService; + /** + * @var CategoryRepositoryInterface + */ + private $categoryRepository; + /** * @param CategoryUrlPathGenerator $categoryUrlPathGenerator * @param ChildrenCategoriesProvider $childrenCategoriesProvider * @param \Magento\CatalogUrlRewrite\Service\V1\StoreViewService $storeViewService + * @param CategoryRepositoryInterface $categoryRepository */ public function __construct( CategoryUrlPathGenerator $categoryUrlPathGenerator, ChildrenCategoriesProvider $childrenCategoriesProvider, - StoreViewService $storeViewService + StoreViewService $storeViewService, + CategoryRepositoryInterface $categoryRepository ) { $this->categoryUrlPathGenerator = $categoryUrlPathGenerator; $this->childrenCategoriesProvider = $childrenCategoriesProvider; $this->storeViewService = $storeViewService; + $this->categoryRepository = $categoryRepository; } /** + * Generate Category Url Path. + * * @param \Magento\Framework\Event\Observer $observer * @return void * @throws \Magento\Framework\Exception\LocalizedException @@ -57,45 +70,68 @@ public function execute(\Magento\Framework\Event\Observer $observer) $useDefaultAttribute = !$category->isObjectNew() && !empty($category->getData('use_default')['url_key']); if ($category->getUrlKey() !== false && !$useDefaultAttribute) { $resultUrlKey = $this->categoryUrlPathGenerator->getUrlKey($category); - if (empty($resultUrlKey)) { - throw new \Magento\Framework\Exception\LocalizedException(__('Invalid URL key')); - } - $category->setUrlKey($resultUrlKey) - ->setUrlPath($this->categoryUrlPathGenerator->getUrlPath($category)); - if (!$category->isObjectNew()) { - $category->getResource()->saveAttribute($category, 'url_path'); - if ($category->dataHasChangedFor('url_path')) { - $this->updateUrlPathForChildren($category); - } + $this->updateUrlKey($category, $resultUrlKey); + } else if ($useDefaultAttribute) { + $resultUrlKey = $category->formatUrlKey($category->getOrigData('name')); + $this->updateUrlKey($category, $resultUrlKey); + $category->setUrlKey(null)->setUrlPath(null); + } + } + + /** + * Update Url Key. + * + * @param Category $category + * @param string $urlKey + * @throws \Magento\Framework\Exception\LocalizedException + */ + private function updateUrlKey(Category $category, string $urlKey) + { + if (empty($urlKey)) { + throw new \Magento\Framework\Exception\LocalizedException(__('Invalid URL key')); + } + $category->setUrlKey($urlKey) + ->setUrlPath($this->categoryUrlPathGenerator->getUrlPath($category)); + if (!$category->isObjectNew()) { + $category->getResource()->saveAttribute($category, 'url_path'); + if ($category->dataHasChangedFor('url_path')) { + $this->updateUrlPathForChildren($category); } } } /** + * Update URL path for children. + * * @param Category $category * @return void */ protected function updateUrlPathForChildren(Category $category) { - $children = $this->childrenCategoriesProvider->getChildren($category, true); - if ($this->isGlobalScope($category->getStoreId())) { - foreach ($children as $child) { + $childrenIds = $this->childrenCategoriesProvider->getChildrenIds($category, true); + foreach ($childrenIds as $childId) { foreach ($category->getStoreIds() as $storeId) { if ($this->storeViewService->doesEntityHaveOverriddenUrlPathForStore( $storeId, - $child->getId(), + $childId, Category::ENTITY )) { - $child->setStoreId($storeId); + $child = $this->categoryRepository->get($childId, $storeId); $this->updateUrlPathForCategory($child); } } } } else { + $children = $this->childrenCategoriesProvider->getChildren($category, true); foreach ($children as $child) { + /** @var Category $child */ $child->setStoreId($category->getStoreId()); - $this->updateUrlPathForCategory($child); + if ($child->getParentId() === $category->getId()) { + $this->updateUrlPathForCategory($child, $category); + } else { + $this->updateUrlPathForCategory($child); + } } } } @@ -112,13 +148,16 @@ protected function isGlobalScope($storeId) } /** + * Update URL path for category. + * * @param Category $category + * @param Category|null $parentCategory * @return void */ - protected function updateUrlPathForCategory(Category $category) + protected function updateUrlPathForCategory(Category $category, Category $parentCategory = null) { $category->unsUrlPath(); - $category->setUrlPath($this->categoryUrlPathGenerator->getUrlPath($category)); + $category->setUrlPath($this->categoryUrlPathGenerator->getUrlPath($category, $parentCategory)); $category->getResource()->saveAttribute($category, 'url_path'); } } diff --git a/app/code/Magento/CatalogUrlRewrite/Observer/UrlRewriteHandler.php b/app/code/Magento/CatalogUrlRewrite/Observer/UrlRewriteHandler.php index 18360dedf0693..b4a35f323e1bc 100644 --- a/app/code/Magento/CatalogUrlRewrite/Observer/UrlRewriteHandler.php +++ b/app/code/Magento/CatalogUrlRewrite/Observer/UrlRewriteHandler.php @@ -24,6 +24,8 @@ use Magento\UrlRewrite\Service\V1\Data\UrlRewrite; /** + * Class for management url rewrites. + * * @SuppressWarnings(PHPMD.CouplingBetweenObjects) */ class UrlRewriteHandler @@ -125,7 +127,7 @@ public function generateProductUrlRewrites(Category $category): array { $mergeDataProvider = clone $this->mergeDataProviderPrototype; $this->isSkippedProduct[$category->getEntityId()] = []; - $saveRewriteHistory = $category->getData('save_rewrites_history'); + $saveRewriteHistory = (bool)$category->getData('save_rewrites_history'); $storeId = (int)$category->getStoreId(); if ($category->getChangedProductIds()) { @@ -156,6 +158,30 @@ public function generateProductUrlRewrites(Category $category): array } /** + * Update product url rewrites for changed product. + * + * @param Category $category + * @return array + */ + public function updateProductUrlRewritesForChangedProduct(Category $category): array + { + $mergeDataProvider = clone $this->mergeDataProviderPrototype; + $this->isSkippedProduct[$category->getEntityId()] = []; + $saveRewriteHistory = (bool)$category->getData('save_rewrites_history'); + $storeIds = $this->getCategoryStoreIds($category); + + if ($category->getChangedProductIds()) { + foreach ($storeIds as $storeId) { + $this->generateChangedProductUrls($mergeDataProvider, $category, (int)$storeId, $saveRewriteHistory); + } + } + + return $mergeDataProvider->getData(); + } + + /** + * Delete category rewrites for children. + * * @param Category $category * @return void */ @@ -184,6 +210,8 @@ public function deleteCategoryRewritesForChildren(Category $category) } /** + * Get category products url rewrites. + * * @param Category $category * @param int $storeId * @param bool $saveRewriteHistory @@ -230,15 +258,15 @@ private function getCategoryProductsUrlRewrites( * * @param MergeDataProvider $mergeDataProvider * @param Category $category - * @param Product $product * @param int $storeId - * @param $saveRewriteHistory + * @param bool $saveRewriteHistory + * @return void */ private function generateChangedProductUrls( MergeDataProvider $mergeDataProvider, Category $category, int $storeId, - $saveRewriteHistory + bool $saveRewriteHistory ) { $this->isSkippedProduct[$category->getEntityId()] = $category->getAffectedProductIds(); diff --git a/app/code/Magento/CatalogUrlRewrite/Test/Mftf/Test/RewriteStoreLevelUrlKeyOfChildCategoryTest.xml b/app/code/Magento/CatalogUrlRewrite/Test/Mftf/Test/RewriteStoreLevelUrlKeyOfChildCategoryTest.xml new file mode 100644 index 0000000000000..ce3f98ee0058a --- /dev/null +++ b/app/code/Magento/CatalogUrlRewrite/Test/Mftf/Test/RewriteStoreLevelUrlKeyOfChildCategoryTest.xml @@ -0,0 +1,66 @@ + + + + + + + <stories value="MAGETWO-89619: #13513: Magento ignore store-level url_key of child category in URL rewrite process for global scope"/> + <description value="Rewriting Store-level URL key of child category"/> + <features value="CatalogUrlRewrite"/> + <severity value="MAJOR"/> + <testCaseId value="MAGETWO-96558"/> + <group value="catalog_url_rewrite"/> + </annotations> + + <before> + <actionGroup ref="LoginAsAdmin" stepKey="loginAsAdmin"/> + <actionGroup ref="AdminCreateStoreViewActionGroup" stepKey="createStoreView" /> + + <createData entity="_defaultCategory" stepKey="defaultCategory"/> + <createData entity="SubCategoryWithParent" stepKey="subCategory"> + <requiredEntity createDataKey="defaultCategory"/> + </createData> + </before> + + <after> + <actionGroup ref="AdminDeleteStoreViewActionGroup" stepKey="deleteStoreView"/> + <actionGroup ref="logout" stepKey="logout"/> + + <deleteData createDataKey="subCategory" stepKey="deleteSubCategory"/> + <deleteData createDataKey="defaultCategory" stepKey="deleteCategory"/> + </after> + + <actionGroup ref="AdminNavigateToCategoryInTree" stepKey="navigateToCreatedSubCategory"> + <argument name="category" value="$$subCategory$$"/> + </actionGroup> + + <actionGroup ref="AdminSwitchStoreViewActionGroup" stepKey="switchToCustomStoreView"/> + + <actionGroup ref="ChangeSeoUrlKeyForSubCategory" stepKey="changeSeoUrlKeyForSubCategory"> + <argument name="value" value="{{BagsCategory.url_key}}"/> + </actionGroup> + + <actionGroup ref="AdminNavigateToCategoryInTree" stepKey="navigateToCreatedDefaultCategory"> + <argument name="category" value="$$defaultCategory$$"/> + </actionGroup> + + <actionGroup ref="ChangeSeoUrlKey" stepKey="changeSeoUrlKeyForDefaultCategory"> + <argument name="value" value="{{GearCategory.url_key}}"/> + </actionGroup> + + <amOnPage url="{{StorefrontHomePage.url}}" stepKey="goToStorefrontPage"/> + + <actionGroup ref="StorefrontSwitchStoreViewActionGroup" stepKey="storefrontSwitchStoreView"/> + + <actionGroup ref="StorefrontGoToSubCategoryPage" stepKey="goToSubCategoryPage"> + <argument name="parentCategory" value="$$defaultCategory$$"/> + <argument name="subCategory" value="$$subCategory$$"/> + <argument name="urlPath" value="{{GearCategory.url_key}}/{{BagsCategory.url_key}}"/> + </actionGroup> + </test> +</tests> diff --git a/app/code/Magento/CatalogUrlRewrite/Test/Unit/Model/Category/ChildrenUrlRewriteGeneratorTest.php b/app/code/Magento/CatalogUrlRewrite/Test/Unit/Model/Category/ChildrenUrlRewriteGeneratorTest.php index 3f641256b1259..f832293706567 100644 --- a/app/code/Magento/CatalogUrlRewrite/Test/Unit/Model/Category/ChildrenUrlRewriteGeneratorTest.php +++ b/app/code/Magento/CatalogUrlRewrite/Test/Unit/Model/Category/ChildrenUrlRewriteGeneratorTest.php @@ -31,6 +31,12 @@ class ChildrenUrlRewriteGeneratorTest extends \PHPUnit\Framework\TestCase /** @var \PHPUnit_Framework_MockObject_MockObject */ private $serializerMock; + /** @var \PHPUnit_Framework_MockObject_MockObject */ + private $categoryRepository; + + /** + * @inheritdoc + */ protected function setUp() { $this->serializerMock = $this->getMockBuilder(Json::class) @@ -47,6 +53,9 @@ protected function setUp() $this->categoryUrlRewriteGenerator = $this->getMockBuilder( \Magento\CatalogUrlRewrite\Model\CategoryUrlRewriteGenerator::class )->disableOriginalConstructor()->getMock(); + $this->categoryRepository = $this->getMockBuilder( + \Magento\Catalog\Model\CategoryRepository::class + )->disableOriginalConstructor()->getMock(); $mergeDataProviderFactory = $this->createPartialMock( \Magento\UrlRewrite\Model\MergeDataProviderFactory::class, ['create'] @@ -59,14 +68,15 @@ protected function setUp() [ 'childrenCategoriesProvider' => $this->childrenCategoriesProvider, 'categoryUrlRewriteGeneratorFactory' => $this->categoryUrlRewriteGeneratorFactory, - 'mergeDataProviderFactory' => $mergeDataProviderFactory + 'mergeDataProviderFactory' => $mergeDataProviderFactory, + 'categoryRepository' => $this->categoryRepository, ] ); } public function testNoChildrenCategories() { - $this->childrenCategoriesProvider->expects($this->once())->method('getChildren')->with($this->category, true) + $this->childrenCategoriesProvider->expects($this->once())->method('getChildrenIds')->with($this->category, true) ->will($this->returnValue([])); $this->assertEquals([], $this->childrenUrlRewriteGenerator->generate('store_id', $this->category)); @@ -76,14 +86,16 @@ public function testGenerate() { $storeId = 'store_id'; $saveRewritesHistory = 'flag'; + $childId = 2; $childCategory = $this->getMockBuilder(\Magento\Catalog\Model\Category::class) ->disableOriginalConstructor()->getMock(); - $childCategory->expects($this->once())->method('setStoreId')->with($storeId); $childCategory->expects($this->once())->method('setData') ->with('save_rewrites_history', $saveRewritesHistory); - $this->childrenCategoriesProvider->expects($this->once())->method('getChildren')->with($this->category, true) - ->will($this->returnValue([$childCategory])); + $this->childrenCategoriesProvider->expects($this->once())->method('getChildrenIds')->with($this->category, true) + ->willReturn([$childId]); + $this->categoryRepository->expects($this->once())->method('get') + ->with($childId, $storeId)->willReturn($childCategory); $this->category->expects($this->any())->method('getData')->with('save_rewrites_history') ->will($this->returnValue($saveRewritesHistory)); $this->categoryUrlRewriteGeneratorFactory->expects($this->once())->method('create') diff --git a/app/code/Magento/CatalogUrlRewrite/Test/Unit/Model/CategoryUrlPathGeneratorTest.php b/app/code/Magento/CatalogUrlRewrite/Test/Unit/Model/CategoryUrlPathGeneratorTest.php index 7297d150a8e6f..e804fb9d08e54 100644 --- a/app/code/Magento/CatalogUrlRewrite/Test/Unit/Model/CategoryUrlPathGeneratorTest.php +++ b/app/code/Magento/CatalogUrlRewrite/Test/Unit/Model/CategoryUrlPathGeneratorTest.php @@ -157,6 +157,61 @@ public function testGetUrlPathWithParent( $this->assertEquals($result, $this->categoryUrlPathGenerator->getUrlPath($this->category)); } + /** + * @return array + */ + public function getUrlPathWithParentCategoryDataProvider(): array + { + $requireGenerationLevel = CategoryUrlPathGenerator::MINIMAL_CATEGORY_LEVEL_FOR_PROCESSING; + $noGenerationLevel = CategoryUrlPathGenerator::MINIMAL_CATEGORY_LEVEL_FOR_PROCESSING - 1; + return [ + [13, 'url-key', false, $requireGenerationLevel, 10, 'parent-path', 'parent-path/url-key'], + [13, 'url-key', false, $requireGenerationLevel, Category::TREE_ROOT_ID, null, 'url-key'], + [13, 'url-key', true, $noGenerationLevel, Category::TREE_ROOT_ID, null, 'url-key'], + ]; + } + + /** + * Test receiving Url Path when parent category is presented. + * + * @param int $parentId + * @param string $urlKey + * @param bool $isCategoryNew + * @param bool $level + * @param int $parentCategoryParentId + * @param null|string $parentUrlPath + * @param string $result + * @dataProvider getUrlPathWithParentCategoryDataProvider + */ + public function testGetUrlPathWithParentCategory( + int $parentId, + string $urlKey, + bool $isCategoryNew, + bool $level, + int $parentCategoryParentId, + $parentUrlPath, + string $result + ) { + $urlPath = null; + $this->category->expects($this->any())->method('getParentId')->willReturn($parentId); + $this->category->expects($this->any())->method('getLevel')->willReturn($level); + $this->category->expects($this->any())->method('getUrlPath')->willReturn($urlPath); + $this->category->expects($this->any())->method('getUrlKey')->willReturn($urlKey); + $this->category->expects($this->any())->method('isObjectNew')->willReturn($isCategoryNew); + + $methods = ['getUrlPath', 'getParentId']; + $parentCategoryMock = $this->createPartialMock(\Magento\Catalog\Model\Category::class, $methods); + $parentCategoryMock->expects($this->any())->method('getParentId')->willReturn($parentCategoryParentId); + $parentCategoryMock->expects($this->any())->method('getUrlPath')->willReturn($parentUrlPath); + + $this->categoryRepository->expects($this->any()) + ->method('get') + ->with($parentCategoryParentId) + ->willReturn($parentCategoryMock); + + $this->assertEquals($result, $this->categoryUrlPathGenerator->getUrlPath($this->category, $parentCategoryMock)); + } + /** * @return array */ diff --git a/app/code/Magento/CatalogUrlRewrite/Test/Unit/Observer/CategoryProcessUrlRewriteSavingObserverTest.php b/app/code/Magento/CatalogUrlRewrite/Test/Unit/Observer/CategoryProcessUrlRewriteSavingObserverTest.php new file mode 100644 index 0000000000000..634dae5643c02 --- /dev/null +++ b/app/code/Magento/CatalogUrlRewrite/Test/Unit/Observer/CategoryProcessUrlRewriteSavingObserverTest.php @@ -0,0 +1,129 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\CatalogUrlRewrite\Test\Unit\Observer; + +use Magento\Catalog\Model\Category; +use Magento\CatalogUrlRewrite\Model\CategoryUrlRewriteGenerator; +use Magento\CatalogUrlRewrite\Model\Map\DatabaseMapPool; +use Magento\CatalogUrlRewrite\Model\Map\DataCategoryUrlRewriteDatabaseMap; +use Magento\CatalogUrlRewrite\Model\Map\DataProductUrlRewriteDatabaseMap; +use Magento\CatalogUrlRewrite\Model\UrlRewriteBunchReplacer; +use Magento\CatalogUrlRewrite\Observer\CategoryProcessUrlRewriteSavingObserver; +use Magento\CatalogUrlRewrite\Observer\UrlRewriteHandler; +use Magento\Framework\Event; +use Magento\Framework\Event\Observer; +use Magento\Framework\TestFramework\Unit\Helper\ObjectManager; +use Magento\Store\Model\ResourceModel\Group\CollectionFactory; + +/** + * Tests Magento\CatalogUrlRewrite\Observer\CategoryProcessUrlRewriteSavingObserver. + */ +class CategoryProcessUrlRewriteSavingObserverTest extends \PHPUnit\Framework\TestCase +{ + /** + * @var CategoryProcessUrlRewriteSavingObserver + */ + private $observer; + + /** + * @var CategoryUrlRewriteGenerator|\PHPUnit_Framework_MockObject_MockObject + */ + private $categoryUrlRewriteGeneratorMock; + + /** + * @var UrlRewriteHandler|\PHPUnit_Framework_MockObject_MockObject + */ + private $urlRewriteHandlerMock; + + /** + * @var UrlRewriteBunchReplacer|\PHPUnit_Framework_MockObject_MockObject $urlRewriteMock + */ + private $urlRewriteBunchReplacerMock; + + /** + * @var DatabaseMapPool|\PHPUnit_Framework_MockObject_MockObject + */ + private $databaseMapPoolMock; + + /** + * @inheritdoc + */ + protected function setUp() + { + $objectManager = new ObjectManager($this); + $this->categoryUrlRewriteGeneratorMock = $this->createMock(CategoryUrlRewriteGenerator::class); + $this->urlRewriteHandlerMock = $this->createMock(UrlRewriteHandler::class); + $this->urlRewriteBunchReplacerMock = $this->createMock(UrlRewriteBunchReplacer::class); + $this->databaseMapPoolMock = $this->createMock(DatabaseMapPool::class); + /** @var CollectionFactory|\PHPUnit_Framework_MockObject_MockObject $storeGroupFactoryMock */ + $storeGroupCollectionFactoryMock = $this->createMock(CollectionFactory::class); + + $this->observer = $objectManager->getObject( + CategoryProcessUrlRewriteSavingObserver::class, + [ + 'categoryUrlRewriteGenerator' => $this->categoryUrlRewriteGeneratorMock, + 'urlRewriteHandler' => $this->urlRewriteHandlerMock, + 'urlRewriteBunchReplacer' => $this->urlRewriteBunchReplacerMock, + 'databaseMapPool' => $this->databaseMapPoolMock, + 'dataUrlRewriteClassNames' => [ + DataCategoryUrlRewriteDatabaseMap::class, + DataProductUrlRewriteDatabaseMap::class + ], + 'storeGroupFactory' => $storeGroupCollectionFactoryMock, + ] + ); + } + + /** + * Covers case when only associated products are changed for category. + * + * @return void + */ + public function testExecuteCategoryOnlyProductHasChanged() + { + $productId = 120; + $productRewrites = ['product-url-rewrite']; + + /** @var Observer|\PHPUnit_Framework_MockObject_MockObject $observerMock */ + $observerMock = $this->createMock(Observer::class); + /** @var Event|\PHPUnit_Framework_MockObject_MockObject $eventMock */ + $eventMock = $this->createMock(Event::class); + /** @var Category|\PHPUnit_Framework_MockObject_MockObject $categoryMock */ + $categoryMock = $this->createPartialMock( + Category::class, + [ + 'hasData', + 'dataHasChangedFor', + 'getChangedProductIds', + ] + ); + + $categoryMock->expects($this->once())->method('hasData')->with('store_id')->willReturn(true); + $categoryMock->expects($this->exactly(2))->method('getChangedProductIds')->willReturn([$productId]); + $categoryMock->expects($this->any())->method('dataHasChangedFor') + ->willReturnMap( + [ + ['url_key', false], + ['is_anchor', false], + ] + ); + $eventMock->expects($this->once())->method('getData')->with('category')->willReturn($categoryMock); + $observerMock->expects($this->once())->method('getEvent')->willReturn($eventMock); + + $this->urlRewriteHandlerMock->expects($this->once()) + ->method('updateProductUrlRewritesForChangedProduct') + ->with($categoryMock) + ->willReturn($productRewrites); + + $this->urlRewriteBunchReplacerMock->expects($this->once()) + ->method('doBunchReplace') + ->with($productRewrites, 10000); + + $this->observer->execute($observerMock); + } +} diff --git a/app/code/Magento/CatalogUrlRewrite/Test/Unit/Observer/CategoryUrlPathAutogeneratorObserverTest.php b/app/code/Magento/CatalogUrlRewrite/Test/Unit/Observer/CategoryUrlPathAutogeneratorObserverTest.php index 1b4d1e08aa208..8ea5dc1ba621b 100644 --- a/app/code/Magento/CatalogUrlRewrite/Test/Unit/Observer/CategoryUrlPathAutogeneratorObserverTest.php +++ b/app/code/Magento/CatalogUrlRewrite/Test/Unit/Observer/CategoryUrlPathAutogeneratorObserverTest.php @@ -49,7 +49,9 @@ protected function setUp() 'getResource', 'getUrlKey', 'getStoreId', - 'getData' + 'getData', + 'getOrigData', + 'formatUrlKey', ]); $this->category->expects($this->any())->method('getResource')->willReturn($this->categoryResource); $this->observer->expects($this->any())->method('getEvent')->willReturnSelf(); @@ -109,7 +111,35 @@ public function testExecuteWithException() $this->categoryUrlPathGenerator->expects($this->once()) ->method('getUrlKey') ->with($this->category) - ->willReturn(null); + ->willReturn(''); + $this->categoryUrlPathAutogeneratorObserver->execute($this->observer); + } + + /** + * Test execute method when option use default url key is true. + */ + public function testExecuteWithUseDefault() + { + $categoryName = 'test'; + $categoryData = ['url_key' => 1]; + $this->category->expects($this->once())->method('getUrlKey')->willReturn($categoryName); + $this->category->expects($this->atLeastOnce()) + ->method('getData') + ->with('use_default') + ->willReturn($categoryData); + $this->category->expects($this->once())->method('getOrigData')->with('name')->willReturn('some_name'); + $this->category->expects($this->once())->method('formatUrlKey')->with('some_name')->willReturn('url_key'); + $this->category->expects($this->any())->method('setUrlKey') + ->willReturnMap([ + ['url_key', $this->category], + [null, $this->category], + ]); + $this->category->expects($this->any())->method('setUrlPath') + ->willReturnMap([ + ['url_path', $this->category], + [null, $this->category], + ]); + $this->categoryUrlPathAutogeneratorObserver->execute($this->observer); } diff --git a/app/code/Magento/CatalogWidget/Test/Mftf/ActionGroup/AdminCreateBlockWithWidgetActionGroup.xml b/app/code/Magento/CatalogWidget/Test/Mftf/ActionGroup/AdminCreateBlockWithWidgetActionGroup.xml new file mode 100644 index 0000000000000..3631e56fcdcf0 --- /dev/null +++ b/app/code/Magento/CatalogWidget/Test/Mftf/ActionGroup/AdminCreateBlockWithWidgetActionGroup.xml @@ -0,0 +1,45 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> + <actionGroup name="AdminCreateCmsBlockWithCatalogProductListWidget"> + <arguments> + <argument name="conditionAttribute" type="string"/> + <argument name="conditionOperator" type="string"/> + <argument name="conditionValue" type="string"/> + </arguments> + <fillField selector="{{AdminCmsBlockContentSection.content}}" userInput="" stepKey="makeContentFieldEmpty"/> + + <click selector="{{AdminCmsBlockContentSection.insertWidgetButton}}" stepKey="clickInsertWidgetButton"/> + <waitForElementVisible selector="{{AdminNewWidgetSection.widgetTypeDropDown}}" time="10" stepKey="waitForInsertWidgetFrame"/> + + <selectOption selector="{{AdminNewWidgetSection.widgetTypeDropDown}}" userInput="Catalog Products List" stepKey="selectCatalogProductListOption"/> + <waitForElementVisible selector="{{AdminNewWidgetSection.addNewCondition}}" stepKey="waitForConditionsElementBecomeAvailable"/> + + <click selector="{{AdminNewWidgetSection.addNewCondition}}" stepKey="clickToAddCondition"/> + <waitForElementVisible selector="{{AdminNewWidgetSection.selectCondition}}" stepKey="waitForSelectBoxOpened"/> + + <selectOption selector="{{AdminNewWidgetSection.selectCondition}}" userInput="{{conditionAttribute}}" stepKey="selectConditionsSelectBox"/> + <waitForElementVisible selector="{{AdminNewWidgetSection.ruleParameter}}" stepKey="seeConditionsAdded"/> + + <click selector="{{AdminNewWidgetSection.conditionOperator}}" stepKey="clickToConditionIs"/> + <selectOption selector="{{AdminNewWidgetSection.conditionOperatorSelect('1')}}" userInput="{{conditionOperator}}" stepKey="selectOperator"/> + + <click selector="{{AdminNewWidgetSection.ruleParameter}}" stepKey="clickAddConditionItem"/> + <waitForElementVisible selector="{{AdminNewWidgetSection.setRuleParameter}}" stepKey="waitForConditionFieldOpened"/> + + <fillField selector="{{AdminNewWidgetSection.setRuleParameter}}" userInput="{{conditionValue}}" stepKey="setConditionValue"/> + <click selector="{{AdminNewWidgetSection.insertWidget}}" stepKey="clickInsertWidget"/> + + <waitForElementVisible selector="{{AdminMainActionsSection.save}}" stepKey="waitForInsertWidgetSaved"/> + <click selector="{{AdminMainActionsSection.save}}" stepKey="clickSaveButton"/> + <see userInput="You saved the block." stepKey="seeSavedBlockMsgOnForm"/> + <conditionalClick selector="{{AdminDataGridHeaderSection.clearFilters}}" dependentSelector="{{AdminDataGridHeaderSection.clearFilters}}" visible="true" stepKey="clearExistingOrderFilters"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/CatalogWidget/Test/Mftf/Test/AdminCatalogProductListWidgetOperatorsTest.xml b/app/code/Magento/CatalogWidget/Test/Mftf/Test/AdminCatalogProductListWidgetOperatorsTest.xml new file mode 100644 index 0000000000000..95cc73ae92d4e --- /dev/null +++ b/app/code/Magento/CatalogWidget/Test/Mftf/Test/AdminCatalogProductListWidgetOperatorsTest.xml @@ -0,0 +1,145 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> + <test name="AdminCatalogProductListWidgetOperatorsTest"> + <annotations> + <features value="CatalogWidget"/> + <stories value="MAGETWO-90930: Problems with operator more/less in the 'catalog Products List' widget"/> + <title value="Checking operator more/less in the 'catalog Products List' widget"/> + <description value="Check 'less than', 'equals or greater than', 'equals or less than' operators"/> + <severity value="MAJOR"/> + <testCaseId value="MAGETWO-96479"/> + <group value="catalogWidget"/> + <group value="WYSIWYGDisabled"/> + </annotations> + + <before> + <createData entity="SimpleSubCategory" stepKey="createSimpleCategory"/> + <createData entity="SimpleProduct" stepKey="createFirstProduct"> + <requiredEntity createDataKey="createSimpleCategory"/> + <field key="price">10</field> + </createData> + <createData entity="SimpleProduct" stepKey="createSecondProduct"> + <requiredEntity createDataKey="createSimpleCategory"/> + <field key="price">50</field> + </createData> + <createData entity="SimpleProduct" stepKey="createThirdProduct"> + <requiredEntity createDataKey="createSimpleCategory"/> + <field key="price">100</field> + </createData> + <createData entity="DefaultCmsBlock" stepKey="createPreReqBlock"/> + <!--User log in on back-end as admin--> + <actionGroup ref="LoginAsAdmin" stepKey="loginAsAdmin"/> + </before> + <after> + <deleteData createDataKey="createPreReqBlock" stepKey="deletePreReqBlock" /> + <deleteData createDataKey="createSimpleCategory" stepKey="deleteSimpleCategory"/> + <deleteData createDataKey="createFirstProduct" stepKey="deleteFirstProduct"/> + <deleteData createDataKey="createSecondProduct" stepKey="deleteSecondProduct"/> + <deleteData createDataKey="createThirdProduct" stepKey="deleteThirdProduct"/> + <actionGroup ref="logout" stepKey="logout"/> + </after> + + <!--Open block with widget--> + <actionGroup ref="NavigateToCreatedCMSBlockPage" stepKey="navigateToCreatedCMSBlockPage1"> + <argument name="cmsBlock" value="$$createPreReqBlock$$"/> + </actionGroup> + + <actionGroup ref="AdminCreateCmsBlockWithCatalogProductListWidget" stepKey="adminCreateBlockWithWidget"> + <argument name="conditionAttribute" value="Price"/> + <argument name="conditionOperator" value="greater than"/> + <argument name="conditionValue" value="20"/> + </actionGroup> + + <!--Go to Catalog > Categories (choose category where created products)--> + <amOnPage url="{{AdminCategoryPage.url}}" stepKey="onCategoryIndexPage"/> + <click selector="{{AdminCategorySidebarTreeSection.expandAll}}" stepKey="clickExpandAll"/> + <waitForElementVisible selector="{{AdminCategorySidebarTreeSection.categoryInTree(SimpleSubCategory.name)}}" stepKey="waitForCategoryVisible"/> + <click selector="{{AdminCategorySidebarTreeSection.categoryInTree(SimpleSubCategory.name)}}" stepKey="clickCategoryLink"/> + + <!--Categories > Content > Add CMS Block: name saved block--> + <waitForElementVisible selector="{{AdminCategoryContentSection.sectionHeader}}" stepKey="waitForContentSection"/> + <conditionalClick selector="{{AdminCategoryContentSection.sectionHeader}}" dependentSelector="{{AdminCategoryContentSection.addCMSBlock}}" visible="false" stepKey="openContentSection"/> + + <selectOption selector="{{AdminCategoryContentSection.addCMSBlock}}" userInput="{{DefaultCmsBlock.title}}" stepKey="selectSavedBlock"/> + + <!--Display Settings > Display Mode: Static block only--> + <waitForElementVisible selector="{{AdminCategoryDisplaySettingsSection.settingsHeader}}" stepKey="waitForDisplaySettingsSection"/> + <conditionalClick selector="{{AdminCategoryDisplaySettingsSection.settingsHeader}}" dependentSelector="{{AdminCategoryDisplaySettingsSection.displayMode}}" visible="false" stepKey="openDisplaySettingsSection"/> + <selectOption userInput="Static block only" selector="{{AdminCategoryDisplaySettingsSection.displayMode}}" stepKey="selectStaticBlockOnlyOption"/> + <click selector="{{AdminMainActionsSection.save}}" stepKey="saveCategoryWithProducts"/> + <see userInput="You saved the category." stepKey="seeSuccessMessage"/> + + <!--Go to Storefront > category--> + <amOnPage url="{{StorefrontCategoryPage.url($$createSimpleCategory.name$$)}}" stepKey="goToStorefrontCategoryPage1"/> + + <!--Check operators Greater than--> + <dontSeeElement selector="{{StorefrontWidgetsSection.checkElementStorefrontByPrice('10')}}" stepKey="dontSeeElementByPrice10"/> + <seeElement selector="{{StorefrontWidgetsSection.checkElementStorefrontByPrice('50')}}" stepKey="seeElementByPrice50"/> + <seeElement selector="{{StorefrontWidgetsSection.checkElementStorefrontByPrice('100')}}" stepKey="seeElementByPrice100"/> + + <!--Open block with widget.--> + <actionGroup ref="NavigateToCreatedCMSBlockPage" stepKey="navigateToCreatedCMSBlockPage2"> + <argument name="cmsBlock" value="$$createPreReqBlock$$"/> + </actionGroup> + + <actionGroup ref="AdminCreateCmsBlockWithCatalogProductListWidget" stepKey="adminCreateBlockWithWidgetLessThan"> + <argument name="conditionAttribute" value="Price"/> + <argument name="conditionOperator" value="less than"/> + <argument name="conditionValue" value="20"/> + </actionGroup> + + <!--Go to Storefront > category--> + <amOnPage url="{{StorefrontCategoryPage.url($$createSimpleCategory.name$$)}}" stepKey="goToStorefrontCategoryPage2"/> + + <!--Check operators Greater than--> + <seeElement selector="{{StorefrontWidgetsSection.checkElementStorefrontByPrice('10')}}" stepKey="seeElementByPrice10"/> + <dontSeeElement selector="{{StorefrontWidgetsSection.checkElementStorefrontByPrice('50')}}" stepKey="dontSeeElementByPrice50"/> + <dontSeeElement selector="{{StorefrontWidgetsSection.checkElementStorefrontByPrice('100')}}" stepKey="dontSeeElementByPrice100"/> + + <!--Open block with widget--> + <actionGroup ref="NavigateToCreatedCMSBlockPage" stepKey="navigateToCreatedCMSBlockPage3"> + <argument name="cmsBlock" value="$$createPreReqBlock$$"/> + </actionGroup> + + <actionGroup ref="AdminCreateCmsBlockWithCatalogProductListWidget" stepKey="adminCreateBlockWithWidgetEqualsOrGreaterThan"> + <argument name="conditionAttribute" value="Price"/> + <argument name="conditionOperator" value="equals or greater than"/> + <argument name="conditionValue" value="50"/> + </actionGroup> + + <!--Go to Storefront > category--> + <amOnPage url="{{StorefrontCategoryPage.url($$createSimpleCategory.name$$)}}" stepKey="goToStorefrontCategoryPage3"/> + + <!--Check operators Greater than--> + <dontSeeElement selector="{{StorefrontWidgetsSection.checkElementStorefrontByPrice('10')}}" stepKey="dontSeeElementByPrice10a"/> + <seeElement selector="{{StorefrontWidgetsSection.checkElementStorefrontByPrice('50')}}" stepKey="seeElementByPrice50a"/> + <seeElement selector="{{StorefrontWidgetsSection.checkElementStorefrontByPrice('100')}}" stepKey="seeElementByPrice100a"/> + + <!--Open block with widget--> + <actionGroup ref="NavigateToCreatedCMSBlockPage" stepKey="navigateToCreatedCMSBlockPage4"> + <argument name="cmsBlock" value="$$createPreReqBlock$$"/> + </actionGroup> + + <actionGroup ref="AdminCreateCmsBlockWithCatalogProductListWidget" stepKey="adminCreateBlockWithWidgetEqualsOrLessThan"> + <argument name="conditionAttribute" value="Price"/> + <argument name="conditionOperator" value="equals or less than"/> + <argument name="conditionValue" value="50"/> + </actionGroup> + + <!--Go to Storefront > category--> + <amOnPage url="{{StorefrontCategoryPage.url($$createSimpleCategory.name$$)}}" stepKey="goToStorefrontCategoryPage4"/> + + <!--Check operators Greater than--> + <seeElement selector="{{StorefrontWidgetsSection.checkElementStorefrontByPrice('10')}}" stepKey="seeElementByPrice10b"/> + <seeElement selector="{{StorefrontWidgetsSection.checkElementStorefrontByPrice('50')}}" stepKey="seeElementByPrice50b"/> + <dontSeeElement selector="{{StorefrontWidgetsSection.checkElementStorefrontByPrice('100')}}" stepKey="dontSeeElementByPrice100b"/> + </test> +</tests> diff --git a/app/code/Magento/Checkout/Api/Data/PaymentDetailsInterface.php b/app/code/Magento/Checkout/Api/Data/PaymentDetailsInterface.php index 45e885d0dbd46..589a94243efa5 100644 --- a/app/code/Magento/Checkout/Api/Data/PaymentDetailsInterface.php +++ b/app/code/Magento/Checkout/Api/Data/PaymentDetailsInterface.php @@ -5,11 +5,13 @@ */ namespace Magento\Checkout\Api\Data; +use Magento\Framework\Api\ExtensibleDataInterface; + /** * Interface PaymentDetailsInterface * @api */ -interface PaymentDetailsInterface +interface PaymentDetailsInterface extends ExtensibleDataInterface { /**#@+ * Constants defined for keys of array, makes typos less likely diff --git a/app/code/Magento/Checkout/Block/Checkout/AttributeMerger.php b/app/code/Magento/Checkout/Block/Checkout/AttributeMerger.php index de996bed02439..bf0884a8c83ea 100644 --- a/app/code/Magento/Checkout/Block/Checkout/AttributeMerger.php +++ b/app/code/Magento/Checkout/Block/Checkout/AttributeMerger.php @@ -6,10 +6,14 @@ namespace Magento\Checkout\Block\Checkout; use Magento\Customer\Api\CustomerRepositoryInterface as CustomerRepository; +use Magento\Customer\Api\Data\CustomerInterface; use Magento\Customer\Helper\Address as AddressHelper; use Magento\Customer\Model\Session; use Magento\Directory\Helper\Data as DirectoryHelper; +/** + * Fields attribute merger. + */ class AttributeMerger { /** @@ -46,6 +50,7 @@ class AttributeMerger 'alpha' => 'validate-alpha', 'numeric' => 'validate-number', 'alphanumeric' => 'validate-alphanum', + 'alphanum-with-spaces' => 'validate-alphanum-with-spaces', 'url' => 'validate-url', 'email' => 'email2', 'length' => 'validate-length', @@ -67,7 +72,7 @@ class AttributeMerger private $customerRepository; /** - * @var \Magento\Customer\Api\Data\CustomerInterface + * @var CustomerInterface */ private $customer; @@ -309,6 +314,8 @@ protected function getMultilineFieldConfig($attributeCode, array $attributeConfi } /** + * Returns default attribute value. + * * @param string $attributeCode * @return null|string */ @@ -346,7 +353,9 @@ protected function getDefaultValue($attributeCode) } /** - * @return \Magento\Customer\Api\Data\CustomerInterface|null + * Returns logged customer. + * + * @return CustomerInterface|null */ protected function getCustomer() { diff --git a/app/code/Magento/Checkout/Block/Onepage.php b/app/code/Magento/Checkout/Block/Onepage.php index ca6b045ddbb5d..e01d5835b4cf0 100644 --- a/app/code/Magento/Checkout/Block/Onepage.php +++ b/app/code/Magento/Checkout/Block/Onepage.php @@ -38,7 +38,7 @@ class Onepage extends \Magento\Framework\View\Element\Template protected $layoutProcessors; /** - * @var \Magento\Framework\Serialize\Serializer\Json + * @var \Magento\Framework\Serialize\SerializerInterface */ private $serializer; @@ -48,8 +48,9 @@ class Onepage extends \Magento\Framework\View\Element\Template * @param \Magento\Checkout\Model\CompositeConfigProvider $configProvider * @param array $layoutProcessors * @param array $data - * @param \Magento\Framework\Serialize\Serializer\Json|null $serializer - * @throws \RuntimeException + * @param \Magento\Framework\Serialize\Serializer\Json $serializer + * @param \Magento\Framework\Serialize\SerializerInterface $serializerInterface + * @SuppressWarnings(PHPMD.UnusedFormalParameter) */ public function __construct( \Magento\Framework\View\Element\Template\Context $context, @@ -57,7 +58,8 @@ public function __construct( \Magento\Checkout\Model\CompositeConfigProvider $configProvider, array $layoutProcessors = [], array $data = [], - \Magento\Framework\Serialize\Serializer\Json $serializer = null + \Magento\Framework\Serialize\Serializer\Json $serializer = null, + \Magento\Framework\Serialize\SerializerInterface $serializerInterface = null ) { parent::__construct($context, $data); $this->formKey = $formKey; @@ -65,12 +67,12 @@ public function __construct( $this->jsLayout = isset($data['jsLayout']) && is_array($data['jsLayout']) ? $data['jsLayout'] : []; $this->configProvider = $configProvider; $this->layoutProcessors = $layoutProcessors; - $this->serializer = $serializer ?: \Magento\Framework\App\ObjectManager::getInstance() - ->get(\Magento\Framework\Serialize\Serializer\Json::class); + $this->serializer = $serializerInterface ?: \Magento\Framework\App\ObjectManager::getInstance() + ->get(\Magento\Framework\Serialize\Serializer\JsonHexTag::class); } /** - * @return string + * @inheritdoc */ public function getJsLayout() { @@ -78,7 +80,7 @@ public function getJsLayout() $this->jsLayout = $processor->process($this->jsLayout); } - return json_encode($this->jsLayout, JSON_HEX_TAG); + return $this->serializer->serialize($this->jsLayout); } /** @@ -115,11 +117,13 @@ public function getBaseUrl() } /** + * Retrieve serialized checkout config. + * * @return bool|string * @since 100.2.0 */ public function getSerializedCheckoutConfig() { - return json_encode($this->getCheckoutConfig(), JSON_HEX_TAG); + return $this->serializer->serialize($this->getCheckoutConfig()); } } diff --git a/app/code/Magento/Checkout/CustomerData/Cart.php b/app/code/Magento/Checkout/CustomerData/Cart.php index ddb077462ef10..01e91d75c02d9 100644 --- a/app/code/Magento/Checkout/CustomerData/Cart.php +++ b/app/code/Magento/Checkout/CustomerData/Cart.php @@ -82,7 +82,7 @@ public function __construct( } /** - * {@inheritdoc} + * @inheritdoc */ public function getSectionData() { @@ -158,11 +158,10 @@ protected function getRecentItems() : $item->getProduct(); $products = $this->catalogUrl->getRewriteByProductStore([$product->getId() => $item->getStoreId()]); - if (!isset($products[$product->getId()])) { - continue; + if (isset($products[$product->getId()])) { + $urlDataObject = new \Magento\Framework\DataObject($products[$product->getId()]); + $item->getProduct()->setUrlDataObject($urlDataObject); } - $urlDataObject = new \Magento\Framework\DataObject($products[$product->getId()]); - $item->getProduct()->setUrlDataObject($urlDataObject); } $items[] = $this->itemPoolInterface->getItemData($item); } diff --git a/app/code/Magento/Checkout/Model/DefaultConfigProvider.php b/app/code/Magento/Checkout/Model/DefaultConfigProvider.php index 04cabf3db89c4..85065599341c1 100644 --- a/app/code/Magento/Checkout/Model/DefaultConfigProvider.php +++ b/app/code/Magento/Checkout/Model/DefaultConfigProvider.php @@ -14,6 +14,7 @@ use Magento\Customer\Model\Session as CustomerSession; use Magento\Customer\Model\Url as CustomerUrlManager; use Magento\Eav\Api\AttributeOptionManagementInterface; +use Magento\Framework\Api\CustomAttributesDataInterface; use Magento\Framework\App\Config\ScopeConfigInterface; use Magento\Framework\App\Http\Context as HttpContext; use Magento\Framework\App\ObjectManager; @@ -22,9 +23,11 @@ use Magento\Framework\UrlInterface; use Magento\Quote\Api\CartItemRepositoryInterface as QuoteItemRepository; use Magento\Quote\Api\CartTotalRepositoryInterface; +use Magento\Quote\Api\Data\AddressInterface; use Magento\Quote\Api\ShippingMethodManagementInterface as ShippingMethodManager; use Magento\Quote\Model\QuoteIdMaskFactory; use Magento\Store\Model\ScopeInterface; +use Magento\Ui\Component\Form\Element\Multiline; /** * Default Config Provider. @@ -268,17 +271,33 @@ public function __construct( } /** - * @inheritdoc + * Return configuration array. + * + * @return array|mixed + * @throws \Magento\Framework\Exception\LocalizedException */ public function getConfig() { - $quoteId = $this->checkoutSession->getQuote()->getId(); + $quote = $this->checkoutSession->getQuote(); + $quoteId = $quote->getId(); + $email = $quote->getShippingAddress()->getEmail(); + $quoteItemData = $this->getQuoteItemData(); $output['formKey'] = $this->formKey->getFormKey(); $output['customerData'] = $this->getCustomerData(); $output['quoteData'] = $this->getQuoteData(); - $output['quoteItemData'] = $this->getQuoteItemData(); + $output['quoteItemData'] = $quoteItemData; + $output['quoteMessages'] = $this->getQuoteItemsMessages($quoteItemData); $output['isCustomerLoggedIn'] = $this->isCustomerLoggedIn(); $output['selectedShippingMethod'] = $this->getSelectedShippingMethod(); + if ($email && !$this->isCustomerLoggedIn()) { + $shippingAddressFromData = $this->getAddressFromData($quote->getShippingAddress()); + $billingAddressFromData = $this->getAddressFromData($quote->getBillingAddress()); + $output['shippingAddressFromData'] = $shippingAddressFromData; + if ($shippingAddressFromData != $billingAddressFromData) { + $output['billingAddressFromData'] = $billingAddressFromData; + } + $output['validatedEmailValue'] = $email; + } $output['storeCode'] = $this->getStoreCode(); $output['isGuestCheckoutAllowed'] = $this->isGuestCheckoutAllowed(); $output['registerUrl'] = $this->getRegisterUrl(); @@ -289,14 +308,15 @@ public function getConfig() $output['staticBaseUrl'] = $this->getStaticBaseUrl(); $output['priceFormat'] = $this->localeFormat->getPriceFormat( null, - $this->checkoutSession->getQuote()->getQuoteCurrencyCode() + $quote->getQuoteCurrencyCode() ); $output['basePriceFormat'] = $this->localeFormat->getPriceFormat( null, - $this->checkoutSession->getQuote()->getBaseCurrencyCode() + $quote->getBaseCurrencyCode() ); $output['postCodes'] = $this->postCodesConfig->getPostCodes(); $output['imageData'] = $this->imageProvider->getImages($quoteId); + $output['totalsData'] = $this->getTotalsData(); $output['shippingPolicy'] = [ 'isEnabled' => $this->scopeConfig->isSetFlag( @@ -431,6 +451,7 @@ private function getQuoteItemData() $quoteItem->getProduct(), 'product_thumbnail_image' )->getUrl(); + $quoteItemData[$index]['message'] = $quoteItem->getMessage(); } } return $quoteItemData; @@ -525,7 +546,40 @@ private function getSelectedShippingMethod() } /** - * Retrieve store code. + * Create address data appropriate to fill checkout address form. + * + * @param AddressInterface $address + * @return array + * @throws \Magento\Framework\Exception\LocalizedException + */ + private function getAddressFromData(AddressInterface $address): array + { + $addressData = []; + $attributesMetadata = $this->addressMetadata->getAllAttributesMetadata(); + foreach ($attributesMetadata as $attributeMetadata) { + if (!$attributeMetadata->isVisible()) { + continue; + } + $attributeCode = $attributeMetadata->getAttributeCode(); + $attributeData = $address->getData($attributeCode); + if ($attributeData) { + if ($attributeMetadata->getFrontendInput() === Multiline::NAME) { + $attributeData = is_array($attributeData) ? $attributeData : explode("\n", $attributeData); + $attributeData = (object)$attributeData; + } + if ($attributeMetadata->isUserDefined()) { + $addressData[CustomAttributesDataInterface::CUSTOM_ATTRIBUTES][$attributeCode] = $attributeData; + continue; + } + $addressData[$attributeCode] = $attributeData; + } + } + + return $addressData; + } + + /** + * Retrieve store code * * @return string * @codeCoverageIgnore @@ -709,4 +763,22 @@ private function getAttributeLabels(array $customAttribute, string $customAttrib return $attributeOptionLabels; } + + /** + * Get notification messages for the quote items + * + * @param array $quoteItemData + * @return array + */ + private function getQuoteItemsMessages(array $quoteItemData): array + { + $quoteItemsMessages = []; + if ($quoteItemData) { + foreach ($quoteItemData as $item) { + $quoteItemsMessages[$item['item_id']] = $item['message']; + } + } + + return $quoteItemsMessages; + } } diff --git a/app/code/Magento/Checkout/Model/ShippingInformationManagement.php b/app/code/Magento/Checkout/Model/ShippingInformationManagement.php index d8142d033f78c..d6fe3ece473df 100644 --- a/app/code/Magento/Checkout/Model/ShippingInformationManagement.php +++ b/app/code/Magento/Checkout/Model/ShippingInformationManagement.php @@ -98,8 +98,8 @@ class ShippingInformationManagement implements \Magento\Checkout\Api\ShippingInf * @param \Magento\Customer\Api\AddressRepositoryInterface $addressRepository * @param \Magento\Framework\App\Config\ScopeConfigInterface $scopeConfig * @param \Magento\Quote\Model\Quote\TotalsCollector $totalsCollector - * @param CartExtensionFactory|null $cartExtensionFactory, - * @param ShippingAssignmentFactory|null $shippingAssignmentFactory, + * @param CartExtensionFactory|null $cartExtensionFactory + * @param ShippingAssignmentFactory|null $shippingAssignmentFactory * @param ShippingFactory|null $shippingFactory * @SuppressWarnings(PHPMD.ExcessiveParameterList) */ @@ -150,6 +150,10 @@ public function saveAddressInformation( $address->setCustomerAddressId(null); } + if ($billingAddress && !$billingAddress->getCustomerAddressId()) { + $billingAddress->setCustomerAddressId(null); + } + if (!$address->getCountryId()) { throw new StateException(__('Shipping address is not set')); } @@ -203,6 +207,8 @@ protected function validateQuote(\Magento\Quote\Model\Quote $quote) } /** + * Prepare shipping assignment. + * * @param CartInterface $quote * @param AddressInterface $address * @param string $method diff --git a/app/code/Magento/Checkout/Observer/SalesQuoteSaveAfterObserver.php b/app/code/Magento/Checkout/Observer/SalesQuoteSaveAfterObserver.php index d926e33d54113..6bc7965ff5e34 100644 --- a/app/code/Magento/Checkout/Observer/SalesQuoteSaveAfterObserver.php +++ b/app/code/Magento/Checkout/Observer/SalesQuoteSaveAfterObserver.php @@ -7,6 +7,9 @@ use Magento\Framework\Event\ObserverInterface; +/** + * Class SalesQuoteSaveAfterObserver + */ class SalesQuoteSaveAfterObserver implements ObserverInterface { /** @@ -24,15 +27,18 @@ public function __construct(\Magento\Checkout\Model\Session $checkoutSession) } /** + * Assign quote to session + * * @param \Magento\Framework\Event\Observer $observer * @return void */ public function execute(\Magento\Framework\Event\Observer $observer) { + /* @var \Magento\Quote\Model\Quote $quote */ $quote = $observer->getEvent()->getQuote(); - /* @var $quote \Magento\Quote\Model\Quote */ + if ($quote->getIsCheckoutCart()) { - $this->checkoutSession->getQuoteId($quote->getId()); + $this->checkoutSession->setQuoteId($quote->getId()); } } } diff --git a/app/code/Magento/Checkout/Test/Mftf/ActionGroup/CheckoutActionGroup.xml b/app/code/Magento/Checkout/Test/Mftf/ActionGroup/CheckoutActionGroup.xml index d98ceda1cda57..7bd6931309148 100644 --- a/app/code/Magento/Checkout/Test/Mftf/ActionGroup/CheckoutActionGroup.xml +++ b/app/code/Magento/Checkout/Test/Mftf/ActionGroup/CheckoutActionGroup.xml @@ -22,6 +22,15 @@ <waitForLoadingMaskToDisappear stepKey="waitForLoadingMaskForNextButton"/> </actionGroup> + <!-- Go to checkout from cart --> + <actionGroup name="GoToCheckoutFromCartActionGroup"> + <waitForElementNotVisible selector="{{StorefrontMinicartSection.emptyCart}}" stepKey="waitUpdateQuantity" /> + <click selector="{{StorefrontMinicartSection.showCart}}" stepKey="clickCart"/> + <waitForLoadingMaskToDisappear stepKey="waitForLoadingMask"/> + <seeInCurrentUrl url="{{CheckoutCartPage.url}}" stepKey="assertCheckoutCartUrl"/> + <click selector="{{StorefrontCheckoutCartSummarySection.proceedToCheckout}}" stepKey="goToCheckout"/> + </actionGroup> + <!-- Checkout place order --> <actionGroup name="CheckoutPlaceOrderActionGroup"> <arguments> @@ -74,4 +83,27 @@ <click selector="{{CheckoutPaymentSection.placeOrder}}" stepKey="clickPlaceOrder"/> <see selector="{{CheckoutSuccessMainSection.successTitle}}" userInput="Thank you for your purchase!" stepKey="waitForLoadSuccessPage"/> </actionGroup> + + <!-- Place order with logged the user --> + <actionGroup name="PlaceOrderWithLoggedUserActionGroup"> + <arguments> + <!--First available shipping method will be selected if value is not passed for shippingMethod--> + <argument name="shippingMethod" defaultValue="" type="string"/> + <!--First available payment method will be selected if value is not passed for paymentMethod--> + <argument name="paymentMethod" defaultValue="" type="string"/> + </arguments> + <waitForElementVisible selector="{{StorefrontCheckoutCartSummarySection.proceedToCheckout}}" stepKey="waitProceedToCheckout"/> + <click selector="{{StorefrontCheckoutCartSummarySection.proceedToCheckout}}" stepKey="clickProceedToCheckout"/> + <click selector="{{CheckoutShippingMethodsSection.checkShippingMethodByName('shippingMethod')}}" stepKey="selectShippingMethod"/> + <waitForElement selector="{{CheckoutShippingSection.next}}" stepKey="waitForNextButton"/> + <click selector="{{CheckoutShippingSection.next}}" stepKey="clickNext"/> + <waitForPageLoad stepKey="waitForPaymentPageLoad"/> + <waitForElement selector="{{CheckoutPaymentSection.paymentSectionTitle}}" stepKey="waitForPaymentSectionLoaded"/> + <seeInCurrentUrl url="{{CheckoutPage.url}}/#payment" stepKey="assertCheckoutPaymentUrl"/> + <conditionalClick selector="{{CheckoutPaymentSection.checkPaymentMethodByName('paymentMethod')}}" dependentSelector="{{CheckoutPaymentSection.checkPaymentMethodByName('paymentMethod')}}" + visible="true" stepKey="checkPaymentMethodIfExist"/> + <waitForElementVisible selector="{{CheckoutPaymentSection.placeOrder}}" stepKey="waitForPlaceOrderButton"/> + <click selector="{{CheckoutPaymentSection.placeOrder}}" stepKey="clickPlaceOrder"/> + <see selector="{{CheckoutSuccessMainSection.successTitle}}" userInput="Thank you for your purchase!" stepKey="waitForLoadSuccessPage"/> + </actionGroup> </actionGroups> diff --git a/app/code/Magento/Checkout/Test/Mftf/ActionGroup/GoToCheckoutFromMinicartActionGroup.xml b/app/code/Magento/Checkout/Test/Mftf/ActionGroup/GoToCheckoutFromMinicartActionGroup.xml index 5d91be6517097..f03be61cccd3a 100644 --- a/app/code/Magento/Checkout/Test/Mftf/ActionGroup/GoToCheckoutFromMinicartActionGroup.xml +++ b/app/code/Magento/Checkout/Test/Mftf/ActionGroup/GoToCheckoutFromMinicartActionGroup.xml @@ -7,12 +7,13 @@ --> <actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" - xsi:noNamespaceSchemaLocation="../../../../../../../dev/tests/acceptance/vendor/magento/magento2-functional-testing-framework/src/Magento/FunctionalTestingFramework/Test/etc/actionGroupSchema.xsd"> + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> <!-- Go to checkout from minicart --> <actionGroup name="GoToCheckoutFromMinicartActionGroup"> <waitForElement selector="{{StorefrontMinicartSection.showCart}}" stepKey="waitMiniCartSectionShow" /> <click selector="{{StorefrontMinicartSection.showCart}}" stepKey="clickCart"/> - <click selector="{{StorefrontMinicartSection.goToCheckout}}" stepKey="goToCheckout"/> + <waitForElementVisible selector="{{StorefrontMinicartSection.goToCheckout}}" time="30" stepKey="waitForGoToCheckoutButtonVisible"/> + <click selector="{{StorefrontMinicartSection.goToCheckout}}" stepKey="clickGoToCheckoutButton"/> <waitForPageLoad stepKey="waitForPageLoad"/> </actionGroup> </actionGroups> diff --git a/app/code/Magento/Checkout/Test/Mftf/ActionGroup/GuestCheckoutFillingShippingSectionActionGroup.xml b/app/code/Magento/Checkout/Test/Mftf/ActionGroup/GuestCheckoutFillingShippingSectionActionGroup.xml index 8ff84e7a436e5..19aeeef8e2bc0 100644 --- a/app/code/Magento/Checkout/Test/Mftf/ActionGroup/GuestCheckoutFillingShippingSectionActionGroup.xml +++ b/app/code/Magento/Checkout/Test/Mftf/ActionGroup/GuestCheckoutFillingShippingSectionActionGroup.xml @@ -32,4 +32,13 @@ <seeInCurrentUrl url="{{CheckoutPage.url}}/#payment" stepKey="assertCheckoutPaymentUrl"/> <waitForLoadingMaskToDisappear stepKey="waitForLoadingMask1"/> </actionGroup> + + <actionGroup name="GuestCheckoutFillingShippingSectionWithoutPaymentsActionGroup" extends="GuestCheckoutFillingShippingSectionActionGroup"> + <waitForElement selector="{{CheckoutPaymentSection.isPaymentSection}}" time="30" stepKey="waitForPaymentSectionLoaded"/> + </actionGroup> + + <actionGroup name="GuestCheckoutFillingShippingSectionWithoutRegionActionGroup" extends="GuestCheckoutFillingShippingSectionActionGroup"> + <selectOption selector="{{CheckoutShippingSection.country}}" userInput="{{customerAddressVar.country}}" after="enterPostcode" stepKey="selectCountry"/> + <remove keyForRemoval="selectRegion"/> + </actionGroup> </actionGroups> diff --git a/app/code/Magento/Checkout/Test/Mftf/ActionGroup/LoggedInUserCheckoutAddNewAddressInShippingSectionActionGroup.xml b/app/code/Magento/Checkout/Test/Mftf/ActionGroup/LoggedInUserCheckoutAddNewAddressInShippingSectionActionGroup.xml index 722e6f1ee49ab..9c4d16ff500a0 100644 --- a/app/code/Magento/Checkout/Test/Mftf/ActionGroup/LoggedInUserCheckoutAddNewAddressInShippingSectionActionGroup.xml +++ b/app/code/Magento/Checkout/Test/Mftf/ActionGroup/LoggedInUserCheckoutAddNewAddressInShippingSectionActionGroup.xml @@ -7,7 +7,7 @@ --> <actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" - xsi:noNamespaceSchemaLocation="../../../../../../../dev/tests/acceptance/vendor/magento/magento2-functional-testing-framework/src/Magento/FunctionalTestingFramework/Test/etc/actionGroupSchema.xsd"> + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> <!-- Logged in user checkout add new adress shipping section --> <actionGroup name="LoggedInUserCheckoutAddNewAddressInShippingSectionActionGroup"> <arguments> @@ -21,6 +21,14 @@ <fillField selector="{{CheckoutShippingSection.addTelephone}}" userInput="{{customerAddressVar.telephone}}" stepKey="enterTelephone"/> <click selector="{{CheckoutShippingSection.addSaveButton}}" stepKey="clickSaveAdressAdd"/> <waitForPageLoad stepKey="waitPageLoad"/> - <see stepKey="seeRegionSelected" selector="{{CheckoutShippingSection.selectedShippingAddress}}" userInput="{{customerAddressVar.state}}"/> + <see selector="{{CheckoutShippingSection.selectedShippingAddress}}" userInput="{{customerAddressVar.state}}" stepKey="seeRegionSelected"/> + </actionGroup> + + <actionGroup name="LoggedInUserCheckoutAddNewShippingSectionWithoutRegionActionGroup" + extends="LoggedInUserCheckoutAddNewAddressInShippingSectionActionGroup"> + <remove keyForRemoval="selectRegion"/> + <remove keyForRemoval="seeRegionSelected"/> + <selectOption selector="{{CheckoutShippingSection.addCountry}}" userInput="{{customerAddressVar.country}}" after="enterPostcode" stepKey="enterCountry"/> + <see selector="{{CheckoutShippingSection.selectedShippingAddress}}" userInput="{{customerAddressVar.city}}" after="waitPageLoad" stepKey="seeCitySelected"/> </actionGroup> </actionGroups> diff --git a/app/code/Magento/Checkout/Test/Mftf/ActionGroup/StorefrontMiniCartActionGroup.xml b/app/code/Magento/Checkout/Test/Mftf/ActionGroup/StorefrontMiniCartActionGroup.xml new file mode 100644 index 0000000000000..7a5c5e1d15872 --- /dev/null +++ b/app/code/Magento/Checkout/Test/Mftf/ActionGroup/StorefrontMiniCartActionGroup.xml @@ -0,0 +1,38 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> + <actionGroup name="clickViewAndEditCartFromMiniCart"> + <conditionalClick selector="{{StorefrontMinicartSection.showCart}}" dependentSelector="{{StorefrontMinicartSection.miniCartOpened}}" visible="false" stepKey="openMiniCart"/> + <waitForElementVisible selector="{{StorefrontMinicartSection.viewAndEditCart}}" stepKey="waitForViewAndEditCartVisible"/> + <click selector="{{StorefrontMinicartSection.viewAndEditCart}}" stepKey="viewAndEditCart"/> + <seeInCurrentUrl url="checkout/cart" stepKey="seeInCurrentUrl"/> + </actionGroup> + <actionGroup name="assertOneProductNameInMiniCart"> + <arguments> + <argument name="productName"/> + </arguments> + <conditionalClick selector="{{StorefrontMinicartSection.showCart}}" dependentSelector="{{StorefrontMinicartSection.miniCartOpened}}" visible="false" stepKey="openMiniCart"/> + <waitForElementVisible selector="{{StorefrontMinicartSection.viewAndEditCart}}" stepKey="waitForViewAndEditCartVisible"/> + <see selector="{{StorefrontMinicartSection.miniCartItemsText}}" userInput="{{productName}}" stepKey="seeInMiniCart"/> + </actionGroup> + + <!--Remove an item from the cart using minicart--> + <actionGroup name="removeProductFromMiniCart"> + <arguments> + <argument name="productName" type="string"/> + </arguments> + <conditionalClick selector="{{StorefrontMinicartSection.showCart}}" dependentSelector="{{StorefrontMinicartSection.miniCartOpened}}" visible="false" stepKey="openMiniCart"/> + <waitForElementVisible selector="{{StorefrontMinicartSection.viewAndEditCart}}" stepKey="waitForMiniCartOpen"/> + <click selector="{{StorefrontMinicartSection.deleteMiniCartItemByName(productName)}}" stepKey="clickDelete"/> + <waitForElementVisible selector="{{StoreFrontRemoveItemModalSection.message}}" stepKey="waitForConfirmationModal"/> + <see selector="{{StoreFrontRemoveItemModalSection.message}}" userInput="Are you sure you would like to remove this item from the shopping cart?" stepKey="seeDeleteConfirmationMessage"/> + <click selector="{{StoreFrontRemoveItemModalSection.ok}}" stepKey="confirmDelete"/> + <waitForPageLoad stepKey="waitForDeleteToFinish"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/Checkout/Test/Mftf/Section/CheckoutCartProductSection.xml b/app/code/Magento/Checkout/Test/Mftf/Section/CheckoutCartProductSection.xml index 2e0b91d50c3ff..f0b58f4263899 100644 --- a/app/code/Magento/Checkout/Test/Mftf/Section/CheckoutCartProductSection.xml +++ b/app/code/Magento/Checkout/Test/Mftf/Section/CheckoutCartProductSection.xml @@ -32,5 +32,8 @@ parameterized="true"/> <element name="removeItem" type="button" selector="//table[@id='shopping-cart-table']//tbody//tr[contains(@class,'item-actions')]//a[contains(@class,'action-delete')]"/> + <element name="productPriceByName" type="text" + selector="//table[@id='shopping-cart-table']//tbody//tr[//a/text()='{{var1}}']//ancestor::td[contains(@class,'price')]//span[@class='price']" + parameterized="true"/> </section> </sections> diff --git a/app/code/Magento/Checkout/Test/Mftf/Section/CheckoutPaymentSection.xml b/app/code/Magento/Checkout/Test/Mftf/Section/CheckoutPaymentSection.xml index c36e23dbb20cc..ae8f9aa5f2aa7 100644 --- a/app/code/Magento/Checkout/Test/Mftf/Section/CheckoutPaymentSection.xml +++ b/app/code/Magento/Checkout/Test/Mftf/Section/CheckoutPaymentSection.xml @@ -39,5 +39,10 @@ <element name="newAddressSelect" type="select" selector=".payment-method._active select[name*='billing_address_id']"/> <element name="goToShipping" type="button" selector="#checkout>ul>li.opc-progress-bar-item._complete>span"/> <element name="orderSummarySubtotal" type="text" selector=".totals.sub span" /> + <element name="billingAddressSameAsShipping" type="checkbox" selector=".payment-method._active [name='billing-address-same-as-shipping']"/> + <element name="orderSummaryTotal" type="text" selector="tr.grand.totals span.price" /> + <element name="checkPaymentMethodByName" type="radio" selector="//div[@id='checkout-payment-method-load']//div[contains(., '{{paymentName}}')]/..//input[@type='radio']" parameterized="true"/> + <element name="orderSummaryShippingTotal" type="text" selector=".totals.shipping.excl span.price"/> + <element name="noPaymentMethods" type="text" selector=".no-quotes-block"/> </section> </sections> diff --git a/app/code/Magento/Checkout/Test/Mftf/Section/CheckoutShippingSection.xml b/app/code/Magento/Checkout/Test/Mftf/Section/CheckoutShippingSection.xml index 2a7437c44eccf..76c9e9f674e6a 100644 --- a/app/code/Magento/Checkout/Test/Mftf/Section/CheckoutShippingSection.xml +++ b/app/code/Magento/Checkout/Test/Mftf/Section/CheckoutShippingSection.xml @@ -19,7 +19,7 @@ <element name="postcode" type="input" selector="input[name=postcode]"/> <element name="country" type="select" selector="select[name=country_id]"/> <element name="telephone" type="input" selector="input[name=telephone]"/> - <element name="firstShippingMethod" type="radio" selector="#checkout-shipping-method-load input[type='radio']"/> + <element name="firstShippingMethod" type="radio" selector="#checkout-shipping-method-load input[type='radio']" timeout="30"/> <element name="selectedShippingAddress" type="text" selector=".shipping-address-item.selected-item"/> <element name="newAddressButton" type="button" selector="#checkout-step-shipping button"/> <element name="next" type="button" selector="[data-role='opc-continue']"/> @@ -30,7 +30,7 @@ <element name="addState" type="select" selector="#shipping-new-address-form select[name='region_id']"/> <element name="addPostcode" type="input" selector="#shipping-new-address-form input[name='postcode']"/> <element name="addTelephone" type="input" selector="#shipping-new-address-form input[name='telephone']"/> - <element name="addcCountry" type="select" selector="#shipping-new-address-form select[name='country_id']"/> + <element name="addCountry" type="select" selector="#shipping-new-address-form select[name='country_id']"/> <element name="addSaveButton" type="button" selector=".action.primary.action-save-address"/> <element name="editActiveAddress" type="button" selector="//div[@class='shipping-address-item selected-item']//span[text()='Edit']" timeout="30"/> </section> diff --git a/app/code/Magento/Checkout/Test/Mftf/Section/StorefrontCheckoutCartSummarySection.xml b/app/code/Magento/Checkout/Test/Mftf/Section/StorefrontCheckoutCartSummarySection.xml index a7c5fb484b2bf..a58fa77bca18b 100644 --- a/app/code/Magento/Checkout/Test/Mftf/Section/StorefrontCheckoutCartSummarySection.xml +++ b/app/code/Magento/Checkout/Test/Mftf/Section/StorefrontCheckoutCartSummarySection.xml @@ -21,5 +21,6 @@ <element name="shippingHeading" type="button" selector="#block-shipping-heading"/> <element name="blockSummary" type="button" selector="#block-summary"/> <element name="discountAmount" type="text" selector="td[data-th='Discount']"/> + <element name="totalsElementByPosition" type="text" selector=".data.table.totals > tbody tr:nth-of-type({{value}}) > th" parameterized="true"/> </section> </sections> diff --git a/app/code/Magento/Checkout/Test/Mftf/Section/StorefrontMinicartSection.xml b/app/code/Magento/Checkout/Test/Mftf/Section/StorefrontMinicartSection.xml index baf098c855eab..00e7d5a011513 100644 --- a/app/code/Magento/Checkout/Test/Mftf/Section/StorefrontMinicartSection.xml +++ b/app/code/Magento/Checkout/Test/Mftf/Section/StorefrontMinicartSection.xml @@ -22,5 +22,8 @@ <element name="viewAndEditCart" type="button" selector=".action.viewcart" timeout="30"/> <element name="miniCartItemsText" type="text" selector=".minicart-items"/> <element name="miniCartSubtotalField" type="text" selector=".block-minicart .amount span.price"/> + <element name="itemQuantity" type="input" selector="//a[text()='{{productName}}']/../..//input[contains(@class,'cart-item-qty')]" parameterized="true"/> + <element name="itemQuantityUpdate" type="button" selector="//a[text()='{{productName}}']/../..//span[text()='Update']" parameterized="true"/> + <element name="emptyCart" type="text" selector=".counter.qty.empty"/> </section> </sections> diff --git a/app/code/Magento/Checkout/Test/Mftf/Test/CheckNotVisibleProductInMinicartTest.xml b/app/code/Magento/Checkout/Test/Mftf/Test/CheckNotVisibleProductInMinicartTest.xml new file mode 100644 index 0000000000000..4b4ca1935fd78 --- /dev/null +++ b/app/code/Magento/Checkout/Test/Mftf/Test/CheckNotVisibleProductInMinicartTest.xml @@ -0,0 +1,70 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> + <test name="CheckNotVisibleProductInMinicartTest"> + <annotations> + <features value="Checkout"/> + <stories value="MAGETWO-96422: Hidden Products are absent in Storefront Mini-Cart" /> + <title value="Not visible individually product in mini-shopping cart."/> + <description value="To be sure that product in mini-shopping cart remains visible after admin makes it not visible individually"/> + <severity value="MAJOR"/> + <group value="checkout"/> + </annotations> + + <!--Create simple product1 and simple product2--> + <createData entity="SimpleTwo" stepKey="createSimpleProduct1"/> + <createData entity="SimpleTwo" stepKey="createSimpleProduct2"/> + + <!--Go to simple product1 page--> + <amOnPage url="$$createSimpleProduct1.custom_attributes[url_key]$$.html" stepKey="navigateToSimpleProductPage1"/> + <waitForPageLoad stepKey="waitForCatalogPageLoad"/> + + <!--Add simple product1 to Shopping Cart--> + <actionGroup ref="addToCartFromStorefrontProductPage" stepKey="addToCartFromStorefrontProductPage1"> + <argument name="productName" value="$$createSimpleProduct1.name$$"/> + </actionGroup> + + <!--Check simple product1 in minicart--> + <comment userInput="Check simple product 1 in minicart" stepKey="commentCheckSimpleProduct1InMinicart" after="addToCartFromStorefrontProductPage1"/> + <actionGroup ref="assertOneProductNameInMiniCart" stepKey="assertProduct1NameInMiniCart"> + <argument name="productName" value="$$createSimpleProduct1.name$$"/> + </actionGroup> + + <!--Make simple product1 not visible individually--> + <updateData entity="SetProductVisibilityHidden" createDataKey="createSimpleProduct1" stepKey="updateSimpleProduct1"> + <requiredEntity createDataKey="createSimpleProduct1"/> + </updateData> + + <!--Go to simple product2 page--> + <amOnPage url="$$createSimpleProduct2.custom_attributes[url_key]$$.html" stepKey="navigateToSimpleProductPage2"/> + <waitForPageLoad stepKey="waitForCatalogPageLoad2"/> + + <!--Add simple product2 to Shopping Cart for updating cart items--> + <actionGroup ref="addToCartFromStorefrontProductPage" stepKey="addToCartFromStorefrontProductPage2"> + <argument name="productName" value="$$createSimpleProduct2.name$$"/> + </actionGroup> + + <!--Check simple product1 in minicart--> + <comment userInput="Check hidden simple product 1 in minicart" stepKey="commentCheckHiddenSimpleProduct1InMinicart" after="addToCartFromStorefrontProductPage2"/> + <actionGroup ref="assertOneProductNameInMiniCart" stepKey="assertHiddenProduct1NameInMiniCart"> + <argument name="productName" value="$$createSimpleProduct1.name$$"/> + </actionGroup> + + <!--Check simple product2 in minicart--> + <comment userInput="Check hidden simple product 2 in minicart" stepKey="commentCheckSimpleProduct2InMinicart" after="addToCartFromStorefrontProductPage2"/> + <actionGroup ref="assertOneProductNameInMiniCart" stepKey="assertProduct2NameInMiniCart"> + <argument name="productName" value="$$createSimpleProduct2.name$$"/> + </actionGroup> + + <!--Delete simple product1 and simple product2--> + <deleteData createDataKey="createSimpleProduct1" stepKey="deleteProduct1"/> + <deleteData createDataKey="createSimpleProduct2" stepKey="deleteProduct2"/> + </test> +</tests> diff --git a/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontCheckoutTotalsSortOrderInCartTest.xml b/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontCheckoutTotalsSortOrderInCartTest.xml new file mode 100644 index 0000000000000..11aae024d2e0a --- /dev/null +++ b/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontCheckoutTotalsSortOrderInCartTest.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="StorefrontCheckoutTotalsSortOrderInCartTest"> + <annotations> + <title value="Checkout Totals Sort Order configuration and displaying in cart"/> + <stories value="MAGETWO-89397: Wrong Checkout Totals Sort Order in cart"/> + <description value="Checkout Totals Sort Order configuration and displaying in cart"/> + <features value="Checkout"/> + <severity value="AVERAGE"/> + <testCaseId value="MAGETWO-96635"/> + <group value="checkout"/> + </annotations> + + <before> + <createData entity="_defaultCategory" stepKey="createCategory"/> + <createData entity="SimpleProduct" stepKey="createProduct"> + <requiredEntity createDataKey="createCategory"/> + </createData> + <createData entity="SaleRule50PercentDiscountNoCoupon" stepKey="createCartRule"/> + <createData entity="CheckoutShippingTotalsSortOrder" stepKey="setConfigShippingTotalsSortOrder"/> + </before> + + <after> + <deleteData createDataKey="createCartRule" stepKey="deleteCartRule"/> + <deleteData createDataKey="createProduct" stepKey="deleteProduct"/> + <deleteData createDataKey="createCategory" stepKey="deleteCategory"/> + <createData entity="DefaultTotalsSortOrder" stepKey="setDefaultTotalsSortOrder"/> + </after> + + <actionGroup ref="AddSimpleProductToCart" stepKey="addProductToCart"> + <argument name="product" value="$$createProduct$$"/> + </actionGroup> + + <actionGroup ref="VerifyDiscountAmount" stepKey="verifyDiscountAmount"> + <argument name="expectedDiscount" value="-$100"/> + </actionGroup> + + <see userInput="Shipping (Flat Rate - Fixed)" selector="{{StorefrontCheckoutCartSummarySection.totalsElementByPosition('3')}}" stepKey="assertElementPosition"/> + </test> +</tests> diff --git a/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontCustomerCheckoutTest.xml b/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontCustomerCheckoutTest.xml index d158bdef6e16a..374ce8dbb425c 100644 --- a/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontCustomerCheckoutTest.xml +++ b/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontCustomerCheckoutTest.xml @@ -77,6 +77,7 @@ <see stepKey="s95" selector="{{OrderDetailsInformationSection.itemsOrdered}}" userInput="$$simpleproduct1.name$$" /> <amOnPage stepKey="s96" url="{{AdminCustomerPage.url}}"/> <waitForPageLoad stepKey="s97"/> + <waitForElementVisible selector="{{AdminCustomerFiltersSection.filtersButton}}" time="30" stepKey="waitFiltersButton"/> <click stepKey="s98" selector="{{AdminCustomerFiltersSection.filtersButton}}"/> <fillField stepKey="s99" selector="{{AdminCustomerFiltersSection.emailInput}}" userInput="$$simpleuscustomer.email$$"/> <click stepKey="s100" selector="{{AdminCustomerFiltersSection.apply}}"/> diff --git a/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontCustomerCheckoutTestWithRestrictedCountriesForPaymentTest.xml b/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontCustomerCheckoutTestWithRestrictedCountriesForPaymentTest.xml new file mode 100644 index 0000000000000..a4583fb7fa50c --- /dev/null +++ b/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontCustomerCheckoutTestWithRestrictedCountriesForPaymentTest.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="StorefrontCustomerCheckoutTestWithRestrictedCountriesForPaymentTest" + extends="StorefrontGuestCheckoutTestWithRestrictedCountriesForPaymentTest"> + <annotations> + <stories value="Checkout via the Storefront"/> + <title value="Checkout via Customer Checkout with restricted countries for payment"/> + <description value="Should be able to place an order as a Customer with restricted countries for payment."/> + <testCaseId value="MC-10831"/> + </annotations> + <before> + <createData entity="Simple_US_Customer" stepKey="createSimpleUsCustomer"/> + </before> + <after> + <actionGroup ref="CustomerLogoutStorefrontActionGroup" stepKey="customerLogoutStorefront"/> + <deleteData createDataKey="createSimpleUsCustomer" stepKey="deleteCustomer"/> + </after> + + <remove keyForRemoval="guestCheckoutFillingShippingSection"/> + <remove keyForRemoval="guestCheckoutFillingShippingSectionUK"/> + <remove keyForRemoval="guestPlaceOrder"/> + + <!-- Login as Customer --> + <actionGroup ref="CustomerLoginOnStorefront" before="goToProductPage" stepKey="customerLogin"> + <argument name="customer" value="$$createSimpleUsCustomer$$" /> + </actionGroup> + + <!-- Select address and go to payments page--> + <see selector="{{CheckoutShippingSection.selectedShippingAddress}}" userInput="{{US_Address_TX.state}}" after="shippingStepIsOpened" stepKey="seeRegion" /> + <waitForElement selector="{{CheckoutShippingMethodsSection.next}}" time="30" after="seeRegion" stepKey="waitNextButton"/> + <actionGroup ref="CheckoutSelectFlatRateShippingMethodActionGroup" after="waitNextButton" stepKey="selectShippingMethod"/> + <click selector="{{CheckoutShippingMethodsSection.next}}" after="selectShippingMethod" stepKey="clickNextButton" /> + <waitForPageLoad after="clickNextButton" stepKey="waitBillingForm"/> + <seeElement selector="{{CheckoutPaymentSection.isPaymentSection}}" after="waitBillingForm" stepKey="checkoutPaymentStepIsOpened"/> + + <!-- Fill UK Address and verify that payment available and checkout successful --> + <click selector="{{CheckoutShippingSection.newAdress}}" after="shippingStepIsOpened1" stepKey="fillNewAddress" /> + <actionGroup ref="LoggedInUserCheckoutAddNewShippingSectionWithoutRegionActionGroup" after="fillNewAddress" stepKey="customerCheckoutFillingShippingSectionUK"> + <argument name="customerVar" value="$$createSimpleUsCustomer$$" /> + <argument name="customerAddressVar" value="UK_Default_Address" /> + </actionGroup> + <waitForElement selector="{{CheckoutShippingMethodsSection.next}}" time="30" after="customerCheckoutFillingShippingSectionUK" stepKey="waitNextButton1"/> + <actionGroup ref="CheckoutSelectFlatRateShippingMethodActionGroup" after="waitNextButton1" stepKey="selectShippingMethod1"/> + <click selector="{{CheckoutShippingMethodsSection.next}}" after="selectShippingMethod1" stepKey="clickNextButton1" /> + + <actionGroup ref="CheckoutPlaceOrderActionGroup" after="selectCheckMoneyOrderPayment" stepKey="customerPlaceorder"> + <argument name="orderNumberMessage" value="CONST.successCheckoutOrderNumberMessage" /> + <argument name="emailYouMessage" value="CONST.successCheckoutEmailYouMessage" /> + </actionGroup> + </test> +</tests> diff --git a/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontGuestCheckoutDataPersistTest.xml b/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontGuestCheckoutDataPersistTest.xml new file mode 100644 index 0000000000000..bebb7be6c9b15 --- /dev/null +++ b/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontGuestCheckoutDataPersistTest.xml @@ -0,0 +1,57 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> + <test name="StorefrontGuestCheckoutDataPersistTest"> + <annotations> + <features value="Checkout"/> + <stories value="MAGETWO-95067: Checkout data (shipping address etc) not persistant after cart update"/> + <title value="Check that checkout data persist after cart update"/> + <description value="Checkout data should be persist after updating cart"/> + <severity value="CRITICAL"/> + <testCaseId value="MAGETWO-96670"/> + <group value="checkout"/> + </annotations> + <before> + <createData entity="_defaultCategory" stepKey="createCategory"/> + <createData entity="_defaultProduct" stepKey="createProduct"> + <requiredEntity createDataKey="createCategory"/> + </createData> + </before> + <after> + <deleteData createDataKey="createCategory" stepKey="deleteCategory"/> + <deleteData createDataKey="createProduct" stepKey="deleteProduct"/> + </after> + + <!-- Add simple product to cart --> + <actionGroup ref="AddSimpleProductToCart" stepKey="addProductToCart"> + <argument name="product" value="$$createProduct$$"/> + </actionGroup> + <!-- Navigate to checkout --> + <actionGroup ref="GoToCheckoutFromMinicartActionGroup" stepKey="goToCheckoutFromMinicart"/> + <!-- Fill shipping address --> + <actionGroup ref="GuestCheckoutFillingShippingSectionActionGroup" stepKey="guestCheckoutFillingShipping"> + <argument name="shippingMethod" value="Flat Rate"/> + </actionGroup> + <!-- Add simple product to cart --> + <actionGroup ref="AddSimpleProductToCart" stepKey="addProductToCart1"> + <argument name="product" value="$$createProduct$$"/> + </actionGroup> + <!-- Navigate to checkout --> + <actionGroup ref="GoToCheckoutFromMinicartActionGroup" stepKey="goToCheckoutFromMinicart1"/> + <seeInField selector="{{GuestCheckoutShippingSection.email}}" userInput="{{CustomerEntityOne.email}}" stepKey="assertGuestEmail"/> + <seeInField selector="{{GuestCheckoutShippingSection.firstName}}" userInput="{{CustomerEntityOne.firstname}}" stepKey="assertGuestFirstName"/> + <seeInField selector="{{GuestCheckoutShippingSection.lastName}}" userInput="{{CustomerEntityOne.lastname}}" stepKey="assertGuestLastName"/> + <seeInField selector="{{GuestCheckoutShippingSection.street}}" userInput="{{CustomerAddressSimple.street[0]}}" stepKey="assertGuestStreet"/> + <seeInField selector="{{GuestCheckoutShippingSection.city}}" userInput="{{CustomerAddressSimple.city}}" stepKey="assertGuestCity"/> + <seeInField selector="{{GuestCheckoutShippingSection.region}}" userInput="{{CustomerAddressSimple.state}}" stepKey="assertGuestRegion"/> + <seeInField selector="{{GuestCheckoutShippingSection.postcode}}" userInput="{{CustomerAddressSimple.postcode}}" stepKey="assertGuestPostcode"/> + <seeInField selector="{{GuestCheckoutShippingSection.telephone}}" userInput="{{CustomerAddressSimple.telephone}}" stepKey="assertGuestTelephone"/> + </test> +</tests> diff --git a/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontGuestCheckoutTest.xml b/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontGuestCheckoutTest.xml index dff957457da95..c5269ca5d0b56 100644 --- a/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontGuestCheckoutTest.xml +++ b/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontGuestCheckoutTest.xml @@ -25,7 +25,10 @@ </createData> </before> <after> - <amOnPage url="admin/admin/auth/logout/" stepKey="amOnLogoutPage"/> + <!--Clear filter in orders grid--> + <actionGroup ref="AdminOrdersGridClearFiltersActionGroup" stepKey="ordersGridClearFilters"/> + <actionGroup ref="logout" stepKey="logoutAdminUserAfterTest"/> + <deleteData createDataKey="createCategory" stepKey="deleteCategory"/> <deleteData createDataKey="createProduct" stepKey="deleteProduct"/> </after> @@ -50,6 +53,7 @@ <click selector="{{GuestCheckoutShippingSection.firstShippingMethod}}" stepKey="selectFirstShippingMethod"/> <waitForElement selector="{{GuestCheckoutShippingSection.next}}" time="30" stepKey="waitForNextButton"/> <click selector="{{GuestCheckoutShippingSection.next}}" stepKey="clickNext"/> + <actionGroup ref="CheckoutSelectCheckMoneyOrderPaymentActionGroup" stepKey="checkPaymentMethod"/> <waitForElement selector="{{GuestCheckoutPaymentSection.placeOrder}}" time="30" stepKey="waitForPlaceOrderButton"/> <conditionalClick selector="{{GuestCheckoutPaymentSection.cartItemsArea}}" dependentSelector="{{GuestCheckoutPaymentSection.cartItemsAreaActive}}" visible="false" stepKey="exposeMiniCart"/> <see selector="{{GuestCheckoutPaymentSection.cartItems}}" userInput="{{_defaultProduct.name}}" stepKey="seeProductInCart"/> @@ -61,13 +65,13 @@ <actionGroup ref="LoginAsAdmin" stepKey="loginAsAdmin1"/> - <amOnPage url="{{AdminOrdersPage.url}}" stepKey="onOrdersPage"/> - <waitForLoadingMaskToDisappear stepKey="waitForLoadingMaskToDisappearOnOrdersPage"/> - <actionGroup ref="clearFiltersAdminDataGrid" stepKey="clearGridFilter"/> - <fillField selector="{{OrdersGridSection.search}}" userInput="{$grabOrderNumber}" stepKey="fillOrderNum"/> - <click selector="{{OrdersGridSection.submitSearch}}" stepKey="submitSearchOrderNum"/> + <!--Navigate to orders index page and filter orders--> + <actionGroup ref="filterOrderGridById" stepKey="filterOrderGridById"> + <argument name="orderId" value="$grabOrderNumber"/> + </actionGroup> <waitForLoadingMaskToDisappear stepKey="waitForLoadingMaskToDisappearOnSearch"/> <click selector="{{OrdersGridSection.firstRow}}" stepKey="clickOrderRow"/> + <waitForPageLoad stepKey="waitForOrderPageLoad"/> <see selector="{{OrderDetailsInformationSection.orderStatus}}" userInput="Pending" stepKey="seeAdminOrderStatus"/> <see selector="{{OrderDetailsInformationSection.accountInformation}}" userInput="Guest" stepKey="seeAdminOrderGuest"/> <see selector="{{OrderDetailsInformationSection.accountInformation}}" userInput="{{CustomerEntityOne.email}}" stepKey="seeAdminOrderEmail"/> @@ -75,4 +79,23 @@ <see selector="{{OrderDetailsInformationSection.shippingAddress}}" userInput="{{CustomerAddressSimple.street[0]}}" stepKey="seeAdminOrderShippingAddress"/> <see selector="{{OrderDetailsInformationSection.itemsOrdered}}" userInput="{{_defaultProduct.name}}" stepKey="seeAdminOrderProduct"/> </test> + <test name="StorefrontGuestCheckoutWithSidebarDisabledTest" extends="StorefrontGuestCheckoutTest"> + <annotations> + <features value="Checkout"/> + <stories value="Checkout via Guest Checkout"/> + <title value="Guest Checkout when Cart sidebar disabled"/> + <description value="Should be able to place an order as a Guest when Cart sidebar is disabled"/> + <severity value="CRITICAL"/> + <testCaseId value="MAGETWO-97155"/> + <group value="checkout"/> + </annotations> + <before> + <magentoCLI command="config:set checkout/sidebar/display 0" stepKey="disableSidebar" /> + </before> + <after> + <magentoCLI command="config:set checkout/sidebar/display 1" stepKey="enableSidebar" /> + </after> + <remove keyForRemoval="addProductNavigateToCheckout" /> + <actionGroup ref="GoToCheckoutFromCartActionGroup" stepKey="guestGoToCheckoutFromCart" after="seeCartQuantity" /> + </test> </tests> diff --git a/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontGuestCheckoutTestWithRestrictedCountriesForPaymentTest.xml b/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontGuestCheckoutTestWithRestrictedCountriesForPaymentTest.xml new file mode 100644 index 0000000000000..3d1dc2cf66689 --- /dev/null +++ b/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontGuestCheckoutTestWithRestrictedCountriesForPaymentTest.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="StorefrontGuestCheckoutTestWithRestrictedCountriesForPaymentTest"> + <annotations> + <features value="Checkout"/> + <stories value="Checkout via Guest Checkout"/> + <title value="Checkout via Guest Checkout with restricted countries for payment"/> + <description value="Should be able to place an order as a Guest with restricted countries for payment."/> + <severity value="MAJOR"/> + <testCaseId value="MC-8243"/> + <group value="checkout"/> + </annotations> + <before> + <createData entity="_defaultCategory" stepKey="createCategory"/> + <createData entity="ApiSimpleProduct" stepKey="createProduct"> + <requiredEntity createDataKey="createCategory"/> + </createData> + <magentoCLI command="config:set payment/checkmo/allowspecific" arguments="1" stepKey="setAllowSpecificCountiesValue" /> + <magentoCLI command="config:set payment/checkmo/specificcountry" arguments="GB" stepKey="setSpecificCountryValue" /> + </before> + <after> + <deleteData createDataKey="createCategory" stepKey="deleteCategory"/> + <deleteData createDataKey="createProduct" stepKey="deleteProduct"/> + <magentoCLI command="config:set payment/checkmo/allowspecific 0" stepKey="unsetAllowSpecificCountiesValue"/> + <magentoCLI command="config:set payment/checkmo/specificcountry ''" stepKey="unsetSpecificCountryValue" /> + </after> + + <!-- Add product to cart --> + <amOnPage url="{{StorefrontProductPage.url($$createProduct.sku$$)}}" stepKey="goToProductPage"/> + <actionGroup ref="addToCartFromStorefrontProductPage" stepKey="addProductToCart"> + <argument name="productName" value="$$createProduct.name$$"/> + </actionGroup> + <!-- Go to checkout page --> + <actionGroup ref="GoToCheckoutFromMinicartActionGroup" stepKey="goToCheckoutFromMinicart" /> + + <!-- Fill US Address and verify that no payment available --> + <seeElement selector="{{CheckoutShippingSection.isShippingStep}}" stepKey="shippingStepIsOpened"/> + <actionGroup ref="GuestCheckoutFillingShippingSectionWithoutPaymentsActionGroup" stepKey="guestCheckoutFillingShippingSection"> + <argument name="customerVar" value="Simple_US_Customer"/> + <argument name="customerAddressVar" value="US_Address_TX"/> + <argument name="shippingMethod" value="Flat Rate" type="string"/> + </actionGroup> + + <waitForElementVisible selector="{{CheckoutPaymentSection.noPaymentMethods}}" stepKey="waitMessage"/> + <see userInput="No Payment method available." stepKey="checkMessage"/> + + <!-- Fill UK Address and verify that payment available and checkout successful --> + <click selector="{{CheckoutHeaderSection.shippingMethodStep}}" stepKey="goToShipping" /> + <waitForElementVisible selector="{{CheckoutShippingSection.isShippingStep}}" stepKey="shippingStepIsOpened1"/> + + <actionGroup ref="GuestCheckoutFillingShippingSectionWithoutRegionActionGroup" stepKey="guestCheckoutFillingShippingSectionUK"> + <argument name="customerVar" value="Simple_US_Customer" /> + <argument name="customerAddressVar" value="UK_Default_Address" /> + <argument name="shippingMethod" value="Flat Rate" type="string"/> + </actionGroup> + <actionGroup ref="CheckoutSelectCheckMoneyOrderPaymentActionGroup" stepKey="selectCheckMoneyOrderPayment" /> + <actionGroup ref="CheckoutPlaceOrderActionGroup" stepKey="guestPlaceOrder"> + <argument name="orderNumberMessage" value="CONST.successGuestCheckoutOrderNumberMessage" /> + <argument name="emailYouMessage" value="CONST.successCheckoutEmailYouMessage" /> + </actionGroup> + </test> +</tests> diff --git a/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontOnePageCheckoutDataWhenChangeQtyTest.xml b/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontOnePageCheckoutDataWhenChangeQtyTest.xml new file mode 100644 index 0000000000000..093435b8c8f26 --- /dev/null +++ b/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontOnePageCheckoutDataWhenChangeQtyTest.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="StorefrontOnePageCheckoutDataWhenChangeQtyTest"> + <annotations> + <features value="Checkout"/> + <stories value="Checkout via Guest Checkout"/> + <title value="One page Checkout Customer data when changing Product Qty"/> + <description value="One page Checkout Customer data when changing Product Qty"/> + <severity value="MAJOR"/> + <testCaseId value="MC-13609"/> + <useCaseId value="MAGETWO-96711"/> + <group value="checkout"/> + </annotations> + <before> + <!--Create a product--> + <createData entity="SimpleProduct3" stepKey="createProduct"/> + </before> + <after> + <!--Delete created data--> + <deleteData createDataKey="createProduct" stepKey="deleteProduct"/> + </after> + + <!--Add product to cart and checkout--> + <amOnPage url="{{StorefrontProductPage.url($$createProduct.name$$)}}" stepKey="amOnSimpleProductPage"/> + <actionGroup ref="addToCartFromStorefrontProductPage" stepKey="addToCartFromStorefrontProductPage"> + <argument name="productName" value="$createProduct.name$$"/> + </actionGroup> + <actionGroup ref="GoToCheckoutFromMinicartActionGroup" stepKey="goToCheckoutFromMinicart"/> + <fillField selector="{{CheckoutShippingSection.email}}" userInput="{{CustomerEntityOne.email}}" stepKey="enterEmail"/> + <fillField selector="{{CheckoutShippingSection.firstName}}" userInput="{{CustomerEntityOne.firstname}}" stepKey="enterFirstName"/> + <fillField selector="{{CheckoutShippingSection.lastName}}" userInput="{{CustomerEntityOne.lastname}}" stepKey="enterLastName"/> + <fillField selector="{{CheckoutShippingSection.street}}" userInput="{{CustomerAddressSimple.street[0]}}" stepKey="enterStreet"/> + <fillField selector="{{CheckoutShippingSection.city}}" userInput="{{CustomerAddressSimple.city}}" stepKey="enterCity"/> + <selectOption selector="{{CheckoutShippingSection.region}}" userInput="{{CustomerAddressSimple.state}}" stepKey="selectRegion"/> + <fillField selector="{{CheckoutShippingSection.postcode}}" userInput="{{CustomerAddressSimple.postcode}}" stepKey="enterPostcode"/> + <fillField selector="{{CheckoutShippingSection.telephone}}" userInput="{{CustomerAddressSimple.telephone}}" stepKey="enterTelephone"/> + <waitForLoadingMaskToDisappear stepKey="waitForLoadingMask"/> + + <!--Grab customer data to check it--> + <grabValueFrom selector="{{CheckoutShippingSection.email}}" stepKey="grabEmail"/> + <grabValueFrom selector="{{CheckoutShippingSection.firstName}}" stepKey="grabFirstName"/> + <grabValueFrom selector="{{CheckoutShippingSection.lastName}}" stepKey="grabLastName"/> + <grabValueFrom selector="{{CheckoutShippingSection.street}}" stepKey="grabStreet"/> + <grabValueFrom selector="{{CheckoutShippingSection.city}}" stepKey="grabCity"/> + <grabTextFrom selector="{{CheckoutShippingSection.region}}" stepKey="grabRegion"/> + <grabValueFrom selector="{{CheckoutShippingSection.postcode}}" stepKey="grabPostcode"/> + <grabValueFrom selector="{{CheckoutShippingSection.telephone}}" stepKey="grabTelephone"/> + + <!--Select shipping method and finalize checkout--> + <actionGroup ref="CheckoutSelectFlatRateShippingMethodActionGroup" stepKey="selectFlatRateShippingMethod"/> + <waitForElement selector="{{CheckoutShippingSection.next}}" time="30" stepKey="waitForNextButton"/> + <click selector="{{CheckoutShippingSection.next}}" stepKey="clickNext"/> + <waitForElement selector="{{CheckoutPaymentSection.paymentSectionTitle}}" time="30" stepKey="waitForPaymentSectionLoaded"/> + <seeInCurrentUrl url="{{CheckoutPage.url}}/#payment" stepKey="assertCheckoutPaymentUrl"/> + + <!--Go to cart page, update qty and proceed to checkout--> + <amOnPage url="{{CheckoutCartPage.url}}" stepKey="goToCartPage"/> + <see userInput="Shopping Cart" stepKey="seeCartPageIsOpened"/> + <fillField selector="{{CheckoutCartProductSection.productQuantityByName($$createProduct.name$$)}}" userInput="2" stepKey="updateProductQty"/> + <click selector="{{CheckoutCartProductSection.updateShoppingCartButton}}" stepKey="clickUpdateShoppingCart"/> + <grabValueFrom selector="{{CheckoutCartProductSection.productQuantityByName($$createProduct.name$$)}}" stepKey="grabQty"/> + <assertEquals expected="2" actual="$grabQty" stepKey="assertQty"/> + <click selector="{{StorefrontCheckoutCartSummarySection.proceedToCheckout}}" stepKey="clickProceedToCheckout"/> + + <!--Check that form is filled with customer data--> + <grabValueFrom selector="{{CheckoutShippingSection.email}}" stepKey="grabEmail1"/> + <grabValueFrom selector="{{CheckoutShippingSection.firstName}}" stepKey="grabFirstName1"/> + <grabValueFrom selector="{{CheckoutShippingSection.lastName}}" stepKey="grabLastName1"/> + <grabValueFrom selector="{{CheckoutShippingSection.street}}" stepKey="grabStreet1"/> + <grabValueFrom selector="{{CheckoutShippingSection.city}}" stepKey="grabCity1"/> + <grabTextFrom selector="{{CheckoutShippingSection.region}}" stepKey="grabRegion1"/> + <grabValueFrom selector="{{CheckoutShippingSection.postcode}}" stepKey="grabPostcode1"/> + <grabValueFrom selector="{{CheckoutShippingSection.telephone}}" stepKey="grabTelephone1"/> + + <assertEquals expected="$grabEmail" actual="$grabEmail1" stepKey="assertEmail"/> + <assertEquals expected="$grabFirstName" actual="$grabFirstName1" stepKey="assertFirstName"/> + <assertEquals expected="$grabLastName" actual="$grabLastName1" stepKey="assertLastName"/> + <assertEquals expected="$grabStreet" actual="$grabStreet1" stepKey="assertStreet"/> + <assertEquals expected="$grabCity" actual="$grabCity1" stepKey="assertCity"/> + <assertEquals expected="$grabRegion" actual="$grabRegion1" stepKey="assertRegion"/> + <assertEquals expected="$grabPostcode" actual="$grabPostcode1" stepKey="assertPostcode"/> + <assertEquals expected="$grabTelephone" actual="$grabTelephone1" stepKey="assertTelephone"/> + </test> +</tests> diff --git a/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontShoppingCartCheckCustomerDefaultShippingAddressForVirtualQuoteTest.xml b/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontShoppingCartCheckCustomerDefaultShippingAddressForVirtualQuoteTest.xml new file mode 100644 index 0000000000000..7750ce0e1686a --- /dev/null +++ b/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontShoppingCartCheckCustomerDefaultShippingAddressForVirtualQuoteTest.xml @@ -0,0 +1,54 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> + <test name="StorefrontShoppingCartCheckCustomerDefaultShippingAddressForVirtualQuoteTest"> + <annotations> + <features value="Checkout"/> + <stories value="Checkout via the Storefront"/> + <title value="Estimator in Shopping cart must be pre-filled by Customer default shipping address for virtual quote"/> + <description value="Estimator in Shopping cart must be pre-filled by Customer default shipping address for virtual quote"/> + <severity value="CRITICAL"/> + <testCaseId value="MAGETWO-78596"/> + <group value="checkout"/> + </annotations> + <before> + <createData entity="VirtualProduct" stepKey="createVirtualProduct"/> + <createData entity="Customer_With_Different_Default_Billing_Shipping_Addresses" stepKey="createCustomer"/> + </before> + <after> + <actionGroup ref="CustomerLogoutStorefrontActionGroup" stepKey="customerLogout"/> + <deleteData createDataKey="createVirtualProduct" stepKey="deleteVirtualProduct"/> + <deleteData createDataKey="createCustomer" stepKey="deleteCustomer"/> + </after> + <!-- Steps --> + <!-- Step 1: Go to Storefront as Customer --> + <actionGroup ref="CustomerLoginOnStorefront" stepKey="customerLogin"> + <argument name="customer" value="$$createCustomer$$" /> + </actionGroup> + <!-- Step 2: Add virtual product to cart --> + <amOnPage url="{{StorefrontProductPage.url($$createVirtualProduct.custom_attributes[url_key]$)}}" stepKey="amOnPage"/> + <actionGroup ref="StorefrontAddProductToCartActionGroup" stepKey="addProductToCart"> + <argument name="product" value="$$createVirtualProduct$$"/> + <argument name="productCount" value="1"/> + </actionGroup> + <!-- Step 3: Go to Shopping Cart --> + <actionGroup ref="StorefrontViewAndEditCartFromMiniCartActionGroup" stepKey="goToShoppingcart"/> + <!-- Step 4: Open Estimate Tax section --> + <click selector="{{StorefrontCheckoutCartSummarySection.estimateShippingAndTax}}" stepKey="openEstimateTaxSection"/> + <seeOptionIsSelected selector="{{StorefrontCheckoutCartSummarySection.country}}" userInput="{{US_Address_CA.country}}" stepKey="checkCountry"/> + <seeOptionIsSelected selector="{{StorefrontCheckoutCartSummarySection.region}}" userInput="{{US_Address_CA.state}}" stepKey="checkState"/> + <scrollTo selector="{{StorefrontCheckoutCartSummarySection.postcode}}" stepKey="scrollToPostCodeField"/> + <grabValueFrom selector="{{StorefrontCheckoutCartSummarySection.postcode}}" stepKey="grabTextPostCode"/> + <assertEquals message="Customer postcode is invalid" stepKey="checkCustomerPostcode"> + <expectedResult type="string">{{US_Address_CA.postcode}}</expectedResult> + <actualResult type="variable">grabTextPostCode</actualResult> + </assertEquals> + </test> +</tests> diff --git a/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontUpdateQtyInShoppingCartAfterUpdateInMinicartTest.xml b/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontUpdateQtyInShoppingCartAfterUpdateInMinicartTest.xml new file mode 100644 index 0000000000000..b0a2c0bfb7e13 --- /dev/null +++ b/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontUpdateQtyInShoppingCartAfterUpdateInMinicartTest.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="StorefrontUpdateQtyInShoppingCartAfterUpdateInMinicartTest"> + <annotations> + <features value="Checkout"/> + <stories value="Checkout via Guest Checkout"/> + <title value="Check updating shopping cart while updating items from minicart"/> + <description value="Check updating shopping cart while updating items from minicart"/> + <severity value="AVERAGE"/> + <testCaseId value="MC-13626"/> + <group value="checkout"/> + </annotations> + <before> + <!--Create category--> + <createData entity="SimpleSubCategory" stepKey="createCategory"/> + <!--Create product--> + <createData entity="SimpleProduct" stepKey="createProduct"> + <requiredEntity createDataKey="createCategory"/> + </createData> + </before> + <after> + <!--Delete product--> + <deleteData createDataKey="createProduct" stepKey="deleteProduct"/> + <!--Delete category--> + <deleteData createDataKey="createCategory" stepKey="deleteCategory"/> + </after> + + <!--Open Product Page--> + <amOnPage url="{{StorefrontProductPage.url($$createProduct.name$$)}}" stepKey="openProductPage"/> + <!--Add product to cart--> + <actionGroup ref="addToCartFromStorefrontProductPage" stepKey="addProductToCart"> + <argument name="productName" value="$$createProduct.name$$"/> + </actionGroup> + + <!--Go to Shopping cart--> + <actionGroup ref="StorefrontOpenCartFromMinicartActionGroup" stepKey="openShoppingCart"/> + <!--Check quantity in Shopping cart--> + <grabValueFrom selector="{{CheckoutCartProductSection.productQuantityByName($$createProduct.name$$)}}" stepKey="grabQtyFromShoppingCart"/> + <assertEquals expected="1" actual="$grabQtyFromShoppingCart" stepKey="assertQtyInShoppingCart"/> + + <!--Open minicart--> + <click selector="{{StorefrontMinicartSection.showCart}}" stepKey="openMiniCart"/> + <waitForElementVisible selector="{{StorefrontMinicartSection.itemQuantity($$createProduct.name$$)}}" stepKey="waitForItemQuantity"/> + <pressKey selector="{{StorefrontMinicartSection.itemQuantity($$createProduct.name$$)}}" parameterArray="[\Facebook\WebDriver\WebDriverKeys::BACKSPACE]" stepKey="clearQtyField"/> + <fillField selector="{{StorefrontMinicartSection.itemQuantity($$createProduct.name$$)}}" userInput="5" stepKey="fillQtyField"/> + <waitForElementVisible selector="{{StorefrontMinicartSection.itemQuantityUpdate($$createProduct.name$$)}}" stepKey="waitForUpdateButton"/> + <click selector="{{StorefrontMinicartSection.itemQuantityUpdate($$createProduct.name$$)}}" stepKey="clickUpdateButton"/> + <waitForAjaxLoad stepKey="waitForAjaxLoad"/> + <!--Check quantity in shopping cart after updating--> + <grabValueFrom selector="{{CheckoutCartProductSection.productQuantityByName($$createProduct.name$$)}}" stepKey="grabQtyFromShoppingCart1"/> + <assertEquals expected="5" actual="$grabQtyFromShoppingCart1" stepKey="assertQtyInShoppingCart1"/> + </test> +</tests> diff --git a/app/code/Magento/Checkout/Test/Unit/Block/Checkout/AttributeMergerTest.php b/app/code/Magento/Checkout/Test/Unit/Block/Checkout/AttributeMergerTest.php new file mode 100644 index 0000000000000..bff2243f30d03 --- /dev/null +++ b/app/code/Magento/Checkout/Test/Unit/Block/Checkout/AttributeMergerTest.php @@ -0,0 +1,122 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Checkout\Test\Unit\Block\Checkout; + +use Magento\Customer\Api\CustomerRepositoryInterface as CustomerRepository; +use Magento\Customer\Helper\Address as AddressHelper; +use Magento\Customer\Model\Session as CustomerSession; +use Magento\Directory\Helper\Data as DirectoryHelper; +use Magento\Checkout\Block\Checkout\AttributeMerger; + +class AttributeMergerTest extends \PHPUnit\Framework\TestCase +{ + /** + * @var CustomerRepository + */ + private $customerRepositoryMock; + + /** + * @var CustomerSession + */ + private $customerSessionMock; + + /** + * @var AddressHelper + */ + private $addressHelperMock; + + /** + * @var DirectoryHelper + */ + private $directoryHelperMock; + + /** + * @var AttributeMerger + */ + private $attributeMerger; + + /** + * @inheritdoc + */ + protected function setUp() + { + + $this->customerRepositoryMock = $this->createMock(CustomerRepository::class); + $this->customerSessionMock = $this->createMock(CustomerSession::class); + $this->addressHelperMock = $this->createMock(AddressHelper::class); + $this->directoryHelperMock = $this->createMock(DirectoryHelper::class); + + $this->attributeMerger = new AttributeMerger( + $this->addressHelperMock, + $this->customerSessionMock, + $this->customerRepositoryMock, + $this->directoryHelperMock + ); + } + + /** + * Tests of element attributes merging. + * + * @param string $validationRule + * @param string $expectedValidation + * @return void + * @dataProvider validationRulesDataProvider + */ + public function testMerge($validationRule, $expectedValidation) + { + $elements = [ + 'field' => [ + 'visible' => true, + 'formElement' => 'input', + 'label' => __('City'), + 'value' => null, + 'sortOrder' => 1, + 'validation' => [ + 'input_validation' => $validationRule, + ], + ] + ]; + + $actualResult = $this->attributeMerger->merge( + $elements, + 'provider', + 'dataScope', + [ + 'field' => + [ + 'validation' => ['length' => true], + ], + ] + ); + + $expectedResult = [ + $expectedValidation => true, + 'length' => true, + ]; + + $this->assertEquals($expectedResult, $actualResult['field']['validation']); + } + + /** + * Provides possible validation types. + * + * @return array + */ + public function validationRulesDataProvider(): array + { + return [ + ['alpha', 'validate-alpha'], + ['numeric', 'validate-number'], + ['alphanumeric', 'validate-alphanum'], + ['alphanum-with-spaces', 'validate-alphanum-with-spaces'], + ['url', 'validate-url'], + ['email', 'email2'], + ['length', 'validate-length'], + ]; + } +} diff --git a/app/code/Magento/Checkout/Test/Unit/Block/OnepageTest.php b/app/code/Magento/Checkout/Test/Unit/Block/OnepageTest.php index 54f77c95148ac..b54339aa2c1d8 100644 --- a/app/code/Magento/Checkout/Test/Unit/Block/OnepageTest.php +++ b/app/code/Magento/Checkout/Test/Unit/Block/OnepageTest.php @@ -35,7 +35,7 @@ class OnepageTest extends \PHPUnit\Framework\TestCase /** * @var \PHPUnit_Framework_MockObject_MockObject */ - private $serializer; + private $serializerMock; protected function setUp() { @@ -49,7 +49,7 @@ protected function setUp() \Magento\Checkout\Block\Checkout\LayoutProcessorInterface::class ); - $this->serializer = $this->createMock(\Magento\Framework\Serialize\Serializer\Json::class); + $this->serializerMock = $this->createMock(\Magento\Framework\Serialize\Serializer\JsonHexTag::class); $this->model = new \Magento\Checkout\Block\Onepage( $contextMock, @@ -57,7 +57,8 @@ protected function setUp() $this->configProviderMock, [$this->layoutProcessorMock], [], - $this->serializer + $this->serializerMock, + $this->serializerMock ); } @@ -93,6 +94,7 @@ public function testGetJsLayout() $processedLayout = ['layout' => ['processed' => true]]; $jsonLayout = '{"layout":{"processed":true}}'; $this->layoutProcessorMock->expects($this->once())->method('process')->with([])->willReturn($processedLayout); + $this->serializerMock->expects($this->once())->method('serialize')->willReturn($jsonLayout); $this->assertEquals($jsonLayout, $this->model->getJsLayout()); } @@ -101,6 +103,7 @@ public function testGetSerializedCheckoutConfig() { $checkoutConfig = ['checkout', 'config']; $this->configProviderMock->expects($this->once())->method('getConfig')->willReturn($checkoutConfig); + $this->serializerMock->expects($this->once())->method('serialize')->willReturn(json_encode($checkoutConfig)); $this->assertEquals(json_encode($checkoutConfig), $this->model->getSerializedCheckoutConfig()); } diff --git a/app/code/Magento/Checkout/Test/Unit/Observer/SalesQuoteSaveAfterObserverTest.php b/app/code/Magento/Checkout/Test/Unit/Observer/SalesQuoteSaveAfterObserverTest.php index 6070bb5d424c1..dabaf173d90b3 100644 --- a/app/code/Magento/Checkout/Test/Unit/Observer/SalesQuoteSaveAfterObserverTest.php +++ b/app/code/Magento/Checkout/Test/Unit/Observer/SalesQuoteSaveAfterObserverTest.php @@ -30,13 +30,14 @@ protected function setUp() public function testSalesQuoteSaveAfter() { + $quoteId = 7; $observer = $this->createMock(\Magento\Framework\Event\Observer::class); $observer->expects($this->once())->method('getEvent')->will( $this->returnValue(new \Magento\Framework\DataObject( - ['quote' => new \Magento\Framework\DataObject(['is_checkout_cart' => 1, 'id' => 7])] + ['quote' => new \Magento\Framework\DataObject(['is_checkout_cart' => 1, 'id' => $quoteId])] )) ); - $this->checkoutSession->expects($this->once())->method('getQuoteId')->with(7); + $this->checkoutSession->expects($this->once())->method('setQuoteId')->with($quoteId); $this->object->execute($observer); } diff --git a/app/code/Magento/Checkout/etc/frontend/di.xml b/app/code/Magento/Checkout/etc/frontend/di.xml index d80f88786c87b..00bcd2a27005a 100644 --- a/app/code/Magento/Checkout/etc/frontend/di.xml +++ b/app/code/Magento/Checkout/etc/frontend/di.xml @@ -59,6 +59,7 @@ <item name="totalsSortOrder" xsi:type="object">Magento\Checkout\Block\Checkout\TotalsProcessor</item> <item name="directoryData" xsi:type="object">Magento\Checkout\Block\Checkout\DirectoryDataProcessor</item> </argument> + <argument name="serializer" xsi:type="object">Magento\Framework\Serialize\Serializer\JsonHexTag</argument> </arguments> </type> <type name="Magento\Checkout\Block\Cart\Totals"> diff --git a/app/code/Magento/Checkout/etc/frontend/sections.xml b/app/code/Magento/Checkout/etc/frontend/sections.xml index 35733a6119a25..90c2878f501cf 100644 --- a/app/code/Magento/Checkout/etc/frontend/sections.xml +++ b/app/code/Magento/Checkout/etc/frontend/sections.xml @@ -46,7 +46,6 @@ </action> <action name="rest/*/V1/guest-carts/*/payment-information"> <section name="cart"/> - <section name="checkout-data"/> </action> <action name="rest/*/V1/guest-carts/*/selected-payment-method"> <section name="cart"/> diff --git a/app/code/Magento/Checkout/i18n/en_US.csv b/app/code/Magento/Checkout/i18n/en_US.csv index 2dcb611c1fe60..bacfe169c1bff 100644 --- a/app/code/Magento/Checkout/i18n/en_US.csv +++ b/app/code/Magento/Checkout/i18n/en_US.csv @@ -181,3 +181,4 @@ Payment,Payment "Item in Cart","Item in Cart" "Items in Cart","Items in Cart" "Close","Close" +"You added %1 to your <a href=""%2"">shopping cart</a>.","You added %1 to your <a href=""%2"">shopping cart</a>." diff --git a/app/code/Magento/Checkout/view/adminhtml/email/failed_payment.html b/app/code/Magento/Checkout/view/adminhtml/email/failed_payment.html index fb55f9b601dc9..03ad7d9e8d848 100644 --- a/app/code/Magento/Checkout/view/adminhtml/email/failed_payment.html +++ b/app/code/Magento/Checkout/view/adminhtml/email/failed_payment.html @@ -23,43 +23,43 @@ <h1>{{trans "Payment Transaction Failed"}}</h1> <ul> <li> - <b>{{trans "Reason"}}</b><br /> + <strong>{{trans "Reason"}}</strong><br /> {{var reason}} </li> <li> - <b>{{trans "Checkout Type"}}</b><br /> + <strong>{{trans "Checkout Type"}}</strong><br /> {{var checkoutType}} </li> <li> - <b>{{trans "Customer:"}}</b><br /> + <strong>{{trans "Customer:"}}</strong><br /> <a href="mailto:{{var customerEmail}}">{{var customer}}</a> <{{var customerEmail}}> </li> <li> - <b>{{trans "Items"}}</b><br /> + <strong>{{trans "Items"}}</strong><br /> {{var items|raw}} </li> <li> - <b>{{trans "Total:"}}</b><br /> + <strong>{{trans "Total:"}}</strong><br /> {{var total}} </li> <li> - <b>{{trans "Billing Address:"}}</b><br /> + <strong>{{trans "Billing Address:"}}</strong><br /> {{var billingAddress.format('html')|raw}} </li> <li> - <b>{{trans "Shipping Address:"}}</b><br /> + <strong>{{trans "Shipping Address:"}}</strong><br /> {{var shippingAddress.format('html')|raw}} </li> <li> - <b>{{trans "Shipping Method:"}}</b><br /> + <strong>{{trans "Shipping Method:"}}</strong><br /> {{var shippingMethod}} </li> <li> - <b>{{trans "Payment Method:"}}</b><br /> + <strong>{{trans "Payment Method:"}}</strong><br /> {{var paymentMethod}} </li> <li> - <b>{{trans "Date & Time:"}}</b><br /> + <strong>{{trans "Date & Time:"}}</strong><br /> {{var dateAndTime}} </li> </ul> diff --git a/app/code/Magento/Checkout/view/frontend/layout/checkout_index_index.xml b/app/code/Magento/Checkout/view/frontend/layout/checkout_index_index.xml index 6c562f0b9027b..642fb5664ddc0 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 @@ -404,6 +404,10 @@ <item name="component" xsi:type="string">Magento_Checkout/js/view/summary/item/details/subtotal</item> <item name="displayArea" xsi:type="string">after_details</item> </item> + <item name="message" xsi:type="array"> + <item name="component" xsi:type="string">Magento_Checkout/js/view/summary/item/details/message</item> + <item name="displayArea" xsi:type="string">item_message</item> + </item> </item> </item> </item> diff --git a/app/code/Magento/Checkout/view/frontend/templates/cart/minicart.phtml b/app/code/Magento/Checkout/view/frontend/templates/cart/minicart.phtml index e6d0260cf2305..20be9cd010c64 100644 --- a/app/code/Magento/Checkout/view/frontend/templates/cart/minicart.phtml +++ b/app/code/Magento/Checkout/view/frontend/templates/cart/minicart.phtml @@ -41,6 +41,14 @@ </div> <?= $block->getChildHtml('minicart.addons') ?> </div> + <?php else: ?> + <script> + require(['jquery'], function ($) { + $('a.action.showcart').click(function() { + $(document.body).trigger('processStart'); + }); + }); + </script> <?php endif ?> <script> window.checkout = <?= /* @escapeNotVerified */ $block->getSerializedConfig() ?>; diff --git a/app/code/Magento/Checkout/view/frontend/web/js/checkout-data.js b/app/code/Magento/Checkout/view/frontend/web/js/checkout-data.js index 22b37b2da0b2f..1858ce946fb07 100644 --- a/app/code/Magento/Checkout/view/frontend/web/js/checkout-data.js +++ b/app/code/Magento/Checkout/view/frontend/web/js/checkout-data.js @@ -10,7 +10,8 @@ */ define([ 'jquery', - 'Magento_Customer/js/customer-data' + 'Magento_Customer/js/customer-data', + 'jquery/jquery-storageapi' ], function ($, storage) { 'use strict'; @@ -23,6 +24,22 @@ define([ storage.set(cacheKey, data); }, + /** + * @return {*} + */ + initData = function () { + return { + 'selectedShippingAddress': null, //Selected shipping address pulled from persistence storage + 'shippingAddressFromData': null, //Shipping address pulled from persistence storage + 'newCustomerShippingAddress': null, //Shipping address pulled from persistence storage for customer + 'selectedShippingRate': null, //Shipping rate pulled from persistence storage + 'selectedPaymentMethod': null, //Payment method pulled from persistence storage + 'selectedBillingAddress': null, //Selected billing address pulled from persistence storage + 'billingAddressFromData': null, //Billing address pulled from persistence storage + 'newCustomerBillingAddress': null //Billing address pulled from persistence storage for new customer + }; + }, + /** * @return {*} */ @@ -30,17 +47,12 @@ define([ var data = storage.get(cacheKey)(); if ($.isEmptyObject(data)) { - data = { - 'selectedShippingAddress': null, //Selected shipping address pulled from persistence storage - 'shippingAddressFromData': null, //Shipping address pulled from persistence storage - 'newCustomerShippingAddress': null, //Shipping address pulled from persistence storage for customer - 'selectedShippingRate': null, //Shipping rate pulled from persistence storage - 'selectedPaymentMethod': null, //Payment method pulled from persistence storage - 'selectedBillingAddress': null, //Selected billing address pulled from persistence storage - 'billingAddressFromData': null, //Billing address pulled from persistence storage - 'newCustomerBillingAddress': null //Billing address pulled from persistence storage for new customer - }; - saveData(data); + data = $.initNamespaceStorage('mage-cache-storage').localStorage.get(cacheKey); + + if ($.isEmptyObject(data)) { + data = initData(); + saveData(data); + } } return data; diff --git a/app/code/Magento/Checkout/view/frontend/web/js/model/checkout-data-resolver.js b/app/code/Magento/Checkout/view/frontend/web/js/model/checkout-data-resolver.js index 28e04699f8daf..bf152f68e25e5 100644 --- a/app/code/Magento/Checkout/view/frontend/web/js/model/checkout-data-resolver.js +++ b/app/code/Magento/Checkout/view/frontend/web/js/model/checkout-data-resolver.js @@ -67,7 +67,15 @@ define([ * Resolve shipping address. Used local storage */ resolveShippingAddress: function () { - var newCustomerShippingAddress = checkoutData.getNewCustomerShippingAddress(); + var newCustomerShippingAddress; + + if (!checkoutData.getShippingAddressFromData() && + window.checkoutConfig.shippingAddressFromData + ) { + checkoutData.setShippingAddressFromData(window.checkoutConfig.shippingAddressFromData); + } + + newCustomerShippingAddress = checkoutData.getNewCustomerShippingAddress(); if (newCustomerShippingAddress) { createShippingAddress(newCustomerShippingAddress); @@ -196,8 +204,17 @@ define([ * Resolve billing address. Used local storage */ resolveBillingAddress: function () { - var selectedBillingAddress = checkoutData.getSelectedBillingAddress(), - newCustomerBillingAddressData = checkoutData.getNewCustomerBillingAddress(); + var selectedBillingAddress, + newCustomerBillingAddressData; + + if (!checkoutData.getBillingAddressFromData() && + window.checkoutConfig.billingAddressFromData + ) { + checkoutData.setBillingAddressFromData(window.checkoutConfig.billingAddressFromData); + } + + selectedBillingAddress = checkoutData.getSelectedBillingAddress(); + newCustomerBillingAddressData = checkoutData.getNewCustomerBillingAddress(); if (selectedBillingAddress) { if (selectedBillingAddress == 'new-customer-address' && newCustomerBillingAddressData) { //eslint-disable-line diff --git a/app/code/Magento/Checkout/view/frontend/web/js/model/place-order.js b/app/code/Magento/Checkout/view/frontend/web/js/model/place-order.js index c3c5b9d68cec0..c07878fcaea92 100644 --- a/app/code/Magento/Checkout/view/frontend/web/js/model/place-order.js +++ b/app/code/Magento/Checkout/view/frontend/web/js/model/place-order.js @@ -9,9 +9,10 @@ define( [ 'mage/storage', 'Magento_Checkout/js/model/error-processor', - 'Magento_Checkout/js/model/full-screen-loader' + 'Magento_Checkout/js/model/full-screen-loader', + 'Magento_Customer/js/customer-data' ], - function (storage, errorProcessor, fullScreenLoader) { + function (storage, errorProcessor, fullScreenLoader, customerData) { 'use strict'; return function (serviceUrl, payload, messageContainer) { @@ -23,6 +24,23 @@ define( function (response) { errorProcessor.process(response, messageContainer); } + ).success( + function (response) { + var clearData = { + 'selectedShippingAddress': null, + 'shippingAddressFromData': null, + 'newCustomerShippingAddress': null, + 'selectedShippingRate': null, + 'selectedPaymentMethod': null, + 'selectedBillingAddress': null, + 'billingAddressFromData': null, + 'newCustomerBillingAddress': null + }; + + if (response.responseType !== 'error') { + customerData.set('checkout-data', clearData); + } + } ).always( function () { fullScreenLoader.stopLoader(); diff --git a/app/code/Magento/Checkout/view/frontend/web/js/model/postcode-validator.js b/app/code/Magento/Checkout/view/frontend/web/js/model/postcode-validator.js index a95471d90dab8..0a5334a42c7e5 100644 --- a/app/code/Magento/Checkout/view/frontend/web/js/model/postcode-validator.js +++ b/app/code/Magento/Checkout/view/frontend/web/js/model/postcode-validator.js @@ -14,11 +14,13 @@ define([ /** * @param {*} postCode * @param {*} countryId + * @param {Array} postCodesPatterns * @return {Boolean} */ - validate: function (postCode, countryId) { - var patterns = window.checkoutConfig.postCodes[countryId], - pattern, regex; + validate: function (postCode, countryId, postCodesPatterns) { + var pattern, regex, + patterns = postCodesPatterns ? postCodesPatterns[countryId] : + window.checkoutConfig.postCodes[countryId]; this.validatedPostCodeExample = []; diff --git a/app/code/Magento/Checkout/view/frontend/web/js/model/shipping-rates-validator.js b/app/code/Magento/Checkout/view/frontend/web/js/model/shipping-rates-validator.js index fde88ebadb393..8b07c02e4d380 100644 --- a/app/code/Magento/Checkout/view/frontend/web/js/model/shipping-rates-validator.js +++ b/app/code/Magento/Checkout/view/frontend/web/js/model/shipping-rates-validator.js @@ -42,6 +42,7 @@ define([ return { validateAddressTimeout: 0, + validateZipCodeTimeout: 0, validateDelay: 2000, /** @@ -133,16 +134,20 @@ define([ }); } else { element.on('value', function () { + clearTimeout(self.validateZipCodeTimeout); + self.validateZipCodeTimeout = setTimeout(function () { + if (element.index === postcodeElementName) { + self.postcodeValidation(element); + } else { + $.each(postcodeElements, function (index, elem) { + self.postcodeValidation(elem); + }); + } + }, delay); + if (!formPopUpState.isVisible()) { clearTimeout(self.validateAddressTimeout); self.validateAddressTimeout = setTimeout(function () { - if (element.index === postcodeElementName) { - self.postcodeValidation(element); - } else { - $.each(postcodeElements, function (index, elem) { - self.postcodeValidation(elem); - }); - } self.validateFields(); }, delay); } 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 dde1ad72ba15e..e66c66006246c 100644 --- a/app/code/Magento/Checkout/view/frontend/web/js/sidebar.js +++ b/app/code/Magento/Checkout/view/frontend/web/js/sidebar.js @@ -25,6 +25,7 @@ define([ } }, scrollHeight: 0, + shoppingCartUrl: window.checkout.shoppingCartUrl, /** * Create sidebar. @@ -227,6 +228,10 @@ define([ if (!_.isUndefined(productData)) { $(document).trigger('ajax:updateCartItemQty'); + + if (window.location.href === this.shoppingCartUrl) { + window.location.reload(false); + } } this._hideItemButton(elem); }, 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 50bbeb30509b7..a195037394085 100644 --- a/app/code/Magento/Checkout/view/frontend/web/js/view/form/element/email.js +++ b/app/code/Magento/Checkout/view/frontend/web/js/view/form/element/email.js @@ -17,7 +17,16 @@ define([ ], function ($, Component, ko, customer, checkEmailAvailability, loginAction, quote, checkoutData, fullScreenLoader) { 'use strict'; - var validatedEmail = checkoutData.getValidatedEmailValue(); + var validatedEmail; + + if (!checkoutData.getValidatedEmailValue() && + window.checkoutConfig.validatedEmailValue + ) { + checkoutData.setInputFieldEmailValue(window.checkoutConfig.validatedEmailValue); + checkoutData.setValidatedEmailValue(window.checkoutConfig.validatedEmailValue); + } + + validatedEmail = checkoutData.getValidatedEmailValue(); if (validatedEmail && !customer.isLoggedIn()) { quote.guestEmail = validatedEmail; 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 df50a5ae94ae9..b4997f9664c81 100644 --- a/app/code/Magento/Checkout/view/frontend/web/js/view/shipping.js +++ b/app/code/Magento/Checkout/view/frontend/web/js/view/shipping.js @@ -247,6 +247,7 @@ define([ */ setShippingInformation: function () { if (this.validateShippingInformation()) { + quote.billingAddress(null); checkoutDataResolver.resolveBillingAddress(); setShippingInformationAction().done( function () { diff --git a/app/code/Magento/Checkout/view/frontend/web/js/view/summary/item/details/message.js b/app/code/Magento/Checkout/view/frontend/web/js/view/summary/item/details/message.js new file mode 100644 index 0000000000000..ed41fd26c47ec --- /dev/null +++ b/app/code/Magento/Checkout/view/frontend/web/js/view/summary/item/details/message.js @@ -0,0 +1,30 @@ +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +define(['uiComponent'], function (Component) { + 'use strict'; + + var quoteMessages = window.checkoutConfig.quoteMessages; + + return Component.extend({ + defaults: { + template: 'Magento_Checkout/summary/item/details/message' + }, + displayArea: 'item_message', + quoteMessages: quoteMessages, + + /** + * @param {Object} item + * @return {null} + */ + getMessage: function (item) { + if (this.quoteMessages[item['item_id']]) { + return this.quoteMessages[item['item_id']]; + } + + return null; + } + }); +}); diff --git a/app/code/Magento/Checkout/view/frontend/web/template/summary/item/details.html b/app/code/Magento/Checkout/view/frontend/web/template/summary/item/details.html index dd59bd78416c6..2491ee12d263c 100644 --- a/app/code/Magento/Checkout/view/frontend/web/template/summary/item/details.html +++ b/app/code/Magento/Checkout/view/frontend/web/template/summary/item/details.html @@ -43,3 +43,6 @@ </div> <!-- /ko --> </div> +<!-- ko foreach: getRegion('item_message') --> + <!-- ko template: getTemplate() --><!-- /ko --> +<!-- /ko --> diff --git a/app/code/Magento/Checkout/view/frontend/web/template/summary/item/details/message.html b/app/code/Magento/Checkout/view/frontend/web/template/summary/item/details/message.html new file mode 100644 index 0000000000000..ea8f58cccd595 --- /dev/null +++ b/app/code/Magento/Checkout/view/frontend/web/template/summary/item/details/message.html @@ -0,0 +1,9 @@ +<!-- +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<div class="cart item message notice" if="getMessage($parents[1])"> + <div data-bind="text: getMessage($parents[1])"></div> +</div> diff --git a/app/code/Magento/Cms/Test/Mftf/ActionGroup/NavigateToCMSPagesActionGroup.xml b/app/code/Magento/Cms/Test/Mftf/ActionGroup/NavigateToCMSPagesActionGroup.xml new file mode 100644 index 0000000000000..6a2012b551407 --- /dev/null +++ b/app/code/Magento/Cms/Test/Mftf/ActionGroup/NavigateToCMSPagesActionGroup.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="NavigateToCreatedCMSBlockPage"> + <arguments> + <argument name="cmsBlock"/> + </arguments> + <amOnPage url="{{AdminCmsBlockGridPage.url}}" stepKey="navigateToCMSBlocksGrid"/> + <conditionalClick selector="{{AdminDataGridHeaderSection.clearFilters}}" dependentSelector="{{AdminDataGridHeaderSection.clearFilters}}" visible="true" stepKey="clickToResetFilters"/> + <click selector="{{AdminDataGridHeaderSection.filters}}" stepKey="clickOnFilters"/> + <fillField userInput="{{cmsBlock.identifier}}" selector="{{AdminDataGridHeaderSection.filterFieldInput('identifier')}}" stepKey="fillFilterAttributeCodeField"/> + <click selector="{{AdminDataGridHeaderSection.applyFilters}}" stepKey="clickApplyFiltersButton"/> + <click selector="{{AdminDataGridTableSection.rowActionSelect}}" stepKey="clickSelectCreatedCMSBlock" /> + <click selector="{{AdminDataGridTableSection.rowEditAction}}" stepKey="navigateToCreatedCMSBlock" /> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/Cms/Test/Mftf/Data/CmsBlockData.xml b/app/code/Magento/Cms/Test/Mftf/Data/CmsBlockData.xml new file mode 100644 index 0000000000000..c8f71253dc6bd --- /dev/null +++ b/app/code/Magento/Cms/Test/Mftf/Data/CmsBlockData.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="DefaultCmsBlock" type="cms_block"> + <data key="title">Default Block</data> + <data key="identifier" unique="suffix" >block</data> + <data key="content">Here is a block test. Yeah!</data> + <data key="active">true</data> + </entity> +</entities> diff --git a/app/code/Magento/Cms/Test/Mftf/Metadata/cms_block-meta.xml b/app/code/Magento/Cms/Test/Mftf/Metadata/cms_block-meta.xml new file mode 100644 index 0000000000000..bab2be6a36155 --- /dev/null +++ b/app/code/Magento/Cms/Test/Mftf/Metadata/cms_block-meta.xml @@ -0,0 +1,23 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<operations xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:DataGenerator/etc/dataOperation.xsd"> + <operation name="CreateCmsBlock" dataType="cms_block" type="create" auth="adminOauth" url="/V1/cmsBlock" method="POST"> + <contentType>application/json</contentType> + <object key="block" dataType="cms_block"> + <field key="title">string</field> + <field key="identifier">string</field> + <field key="content">string</field> + <field key="active">true</field> + </object> + </operation> + + <operation name="DeleteCmsBlock" dataType="cms_block" type="delete" auth="adminOauth" url="/V1/cmsBlock/{id}" method="DELETE"> + <contentType>application/json</contentType> + </operation> +</operations> diff --git a/app/code/Magento/Cms/Test/Mftf/Page/AdminCmsBlockEditPage.xml b/app/code/Magento/Cms/Test/Mftf/Page/AdminCmsBlockEditPage.xml new file mode 100644 index 0000000000000..a2e16b8f279df --- /dev/null +++ b/app/code/Magento/Cms/Test/Mftf/Page/AdminCmsBlockEditPage.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="AdminCmsBlockEditPage" url="/cms/block/edit/id/{{var1}}" area="admin" module="Magento_Cms" parameterized="true"> + <section name="AdminCmsBlockContentSection" /> + </page> +</pages> diff --git a/app/code/Magento/Cms/Test/Mftf/Page/AdminCmsBlockGridPage.xml b/app/code/Magento/Cms/Test/Mftf/Page/AdminCmsBlockGridPage.xml new file mode 100644 index 0000000000000..2cabe182714dc --- /dev/null +++ b/app/code/Magento/Cms/Test/Mftf/Page/AdminCmsBlockGridPage.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="AdminCmsBlockGridPage" url="/cms/block/" area="admin" module="Magento_Cms"> + </page> +</pages> diff --git a/app/code/Magento/Cms/Test/Mftf/Section/AdminCmsBlockContentSection.xml b/app/code/Magento/Cms/Test/Mftf/Section/AdminCmsBlockContentSection.xml new file mode 100644 index 0000000000000..9614f13f9e3d3 --- /dev/null +++ b/app/code/Magento/Cms/Test/Mftf/Section/AdminCmsBlockContentSection.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="AdminCmsBlockContentSection"> + <element name="content" type="textarea" selector="#cms_block_form_content"/> + <element name="insertWidgetButton" type="button" selector=".scalable.action-add-widget.plugin"/> + </section> +</sections> diff --git a/app/code/Magento/Cms/Test/Mftf/Section/CmsNewPagePageBasicFieldsSection.xml b/app/code/Magento/Cms/Test/Mftf/Section/CmsNewPagePageBasicFieldsSection.xml index cf07d1003b7c2..1d9e58e870cfb 100644 --- a/app/code/Magento/Cms/Test/Mftf/Section/CmsNewPagePageBasicFieldsSection.xml +++ b/app/code/Magento/Cms/Test/Mftf/Section/CmsNewPagePageBasicFieldsSection.xml @@ -7,8 +7,9 @@ --> <sections xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" - xsi:noNamespaceSchemaLocation="../../../../../../../dev/tests/acceptance/vendor/magento/magento2-functional-testing-framework/src/Magento/FunctionalTestingFramework/Page/etc/SectionObject.xsd"> + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Page/etc/SectionObject.xsd"> <section name="CmsNewPagePageBasicFieldsSection"> <element name="pageTitle" type="input" selector="input[name=title]"/> + <element name="requiredFieldIndicator" type="text" selector=" return window.getComputedStyle(document.querySelector('._required[data-index=title]>.admin__field-label span'), ':after').getPropertyValue('content');"/> </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 63069c2ccf264..370e7af15d4b2 100644 --- a/app/code/Magento/Cms/Test/Mftf/Section/CmsPagesPageActionsSection.xml +++ b/app/code/Magento/Cms/Test/Mftf/Section/CmsPagesPageActionsSection.xml @@ -7,7 +7,7 @@ --> <sections xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" - xsi:noNamespaceSchemaLocation="../../../../../../../dev/tests/acceptance/vendor/magento/magento2-functional-testing-framework/src/Magento/FunctionalTestingFramework/Page/etc/SectionObject.xsd"> + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Page/etc/SectionObject.xsd"> <section name="CmsPagesPageActionsSection"> <element name="addNewPage" 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"/> diff --git a/app/code/Magento/Config/Block/System/Config/Form.php b/app/code/Magento/Config/Block/System/Config/Form.php index 81e39a83296d7..a05151153daa2 100644 --- a/app/code/Magento/Config/Block/System/Config/Form.php +++ b/app/code/Magento/Config/Block/System/Config/Form.php @@ -143,13 +143,15 @@ public function __construct( \Magento\Config\Model\Config\Structure $configStructure, \Magento\Config\Block\System\Config\Form\Fieldset\Factory $fieldsetFactory, \Magento\Config\Block\System\Config\Form\Field\Factory $fieldFactory, - array $data = [] + array $data = [], + SettingChecker $settingChecker = null ) { parent::__construct($context, $registry, $formFactory, $data); $this->_configFactory = $configFactory; $this->_configStructure = $configStructure; $this->_fieldsetFactory = $fieldsetFactory; $this->_fieldFactory = $fieldFactory; + $this->settingChecker = $settingChecker ?: ObjectManager::getInstance()->get(SettingChecker::class); $this->_scopeLabels = [ self::SCOPE_DEFAULT => __('[GLOBAL]'), @@ -158,18 +160,6 @@ public function __construct( ]; } - /** - * @deprecated 100.1.2 - * @return SettingChecker - */ - private function getSettingChecker() - { - if ($this->settingChecker === null) { - $this->settingChecker = ObjectManager::getInstance()->get(SettingChecker::class); - } - return $this->settingChecker; - } - /** * Initialize objects required to render config form * @@ -366,9 +356,8 @@ protected function _initElement( $sharedClass = $this->_getSharedCssClass($field); $requiresClass = $this->_getRequiresCssClass($field, $fieldPrefix); + $isReadOnly = $this->isReadOnly($field, $path); - $isReadOnly = $this->getElementVisibility()->isDisabled($field->getPath()) - ?: $this->getSettingChecker()->isReadOnly($path, $this->getScope(), $this->getStringScopeCode()); $formField = $fieldset->addField( $elementId, $field->getType(), @@ -417,7 +406,7 @@ private function getFieldData(\Magento\Config\Model\Config\Structure\Element\Fie { $data = $this->getAppConfigDataValue($path); - $placeholderValue = $this->getSettingChecker()->getPlaceholderValue( + $placeholderValue = $this->settingChecker->getPlaceholderValue( $path, $this->getScope(), $this->getStringScopeCode() @@ -718,6 +707,7 @@ protected function _getAdditionalElementTypes() * * @TODO delete this methods when {^see above^} is done * @return string + * @SuppressWarnings(PHPMD.RequestAwareBlockMethod) */ public function getSectionCode() { @@ -729,6 +719,7 @@ public function getSectionCode() * * @TODO delete this methods when {^see above^} is done * @return string + * @SuppressWarnings(PHPMD.RequestAwareBlockMethod) */ public function getWebsiteCode() { @@ -740,6 +731,7 @@ public function getWebsiteCode() * * @TODO delete this methods when {^see above^} is done * @return string + * @SuppressWarnings(PHPMD.RequestAwareBlockMethod) */ public function getStoreCode() { @@ -797,6 +789,26 @@ private function getAppConfig() return $this->appConfig; } + /** + * Check Path is Readonly + * + * @param \Magento\Config\Model\Config\Structure\Element\Field $field + * @param string $path + * @return boolean + */ + private function isReadOnly(\Magento\Config\Model\Config\Structure\Element\Field $field, $path) + { + $isReadOnly = $this->settingChecker->isReadOnly( + $path, + ScopeConfigInterface::SCOPE_TYPE_DEFAULT + ); + if (!$isReadOnly) { + $isReadOnly = $this->getElementVisibility()->isDisabled($field->getPath()) + ?: $this->settingChecker->isReadOnly($path, $this->getScope(), $this->getStringScopeCode()); + } + return $isReadOnly; + } + /** * Retrieve deployment config data value by path * diff --git a/app/code/Magento/Config/Console/Command/ConfigSet/DefaultProcessor.php b/app/code/Magento/Config/Console/Command/ConfigSet/DefaultProcessor.php index d7d513bfad423..86ae1f96749df 100644 --- a/app/code/Magento/Config/Console/Command/ConfigSet/DefaultProcessor.php +++ b/app/code/Magento/Config/Console/Command/ConfigSet/DefaultProcessor.php @@ -7,17 +7,19 @@ use Magento\Config\App\Config\Type\System; use Magento\Config\Console\Command\ConfigSetCommand; +use Magento\Config\Model\Config\Factory as ConfigFactory; use Magento\Framework\App\Config\ConfigPathResolver; use Magento\Framework\App\DeploymentConfig; +use Magento\Framework\App\ObjectManager; use Magento\Framework\Exception\CouldNotSaveException; use Magento\Config\Model\PreparedValueFactory; -use Magento\Framework\App\Config\Value; /** * Processes default flow of config:set command. + * * This processor saves the value of configuration into database. * - * {@inheritdoc} + * @inheritdoc * @api * @since 100.2.0 */ @@ -44,26 +46,36 @@ class DefaultProcessor implements ConfigSetProcessorInterface */ private $preparedValueFactory; + /** + * @var ConfigFactory + */ + private $configFactory; + /** * @param PreparedValueFactory $preparedValueFactory The factory for prepared value * @param DeploymentConfig $deploymentConfig The deployment configuration reader * @param ConfigPathResolver $configPathResolver The resolver for configuration paths according to source type + * @param ConfigFactory|null $configFactory */ public function __construct( PreparedValueFactory $preparedValueFactory, DeploymentConfig $deploymentConfig, - ConfigPathResolver $configPathResolver + ConfigPathResolver $configPathResolver, + ConfigFactory $configFactory = null ) { $this->preparedValueFactory = $preparedValueFactory; $this->deploymentConfig = $deploymentConfig; $this->configPathResolver = $configPathResolver; + + $this->configFactory = $configFactory ?? ObjectManager::getInstance()->get(ConfigFactory::class); } /** * Processes database flow of config:set command. + * * Requires installed application. * - * {@inheritdoc} + * @inheritdoc * @since 100.2.0 */ public function process($path, $value, $scope, $scopeCode) @@ -78,12 +90,12 @@ public function process($path, $value, $scope, $scopeCode) } try { - /** @var Value $backendModel */ - $backendModel = $this->preparedValueFactory->create($path, $value, $scope, $scopeCode); - if ($backendModel instanceof Value) { - $resourceModel = $backendModel->getResource(); - $resourceModel->save($backendModel); - } + $config = $this->configFactory->create([ + 'scope' => $scope, + 'scope_code' => $scopeCode, + ]); + $config->setDataByPath($path, $value); + $config->save(); } catch (\Exception $exception) { throw new CouldNotSaveException(__('%1', $exception->getMessage()), $exception); } diff --git a/app/code/Magento/Config/Model/Config.php b/app/code/Magento/Config/Model/Config.php index 0472c5daa276f..b1074e92cc949 100644 --- a/app/code/Magento/Config/Model/Config.php +++ b/app/code/Magento/Config/Model/Config.php @@ -9,15 +9,32 @@ use Magento\Config\Model\Config\Structure\Element\Group; use Magento\Config\Model\Config\Structure\Element\Field; use Magento\Framework\App\ObjectManager; +use Magento\Framework\App\ScopeInterface; +use Magento\Framework\App\ScopeResolverPool; +use Magento\Store\Model\ScopeInterface as StoreScopeInterface; +use Magento\Store\Model\ScopeTypeNormalizer; /** * Backend config model + * * Used to save configuration * * @author Magento Core Team <core@magentocommerce.com> * @SuppressWarnings(PHPMD.CouplingBetweenObjects) * @api * @since 100.0.2 + * @method string getSection() + * @method void setSection(string $section) + * @method string getWebsite() + * @method void setWebsite(string $website) + * @method string getStore() + * @method void setStore(string $store) + * @method string getScope() + * @method void setScope(string $scope) + * @method int getScopeId() + * @method void setScopeId(int $scopeId) + * @method string getScopeCode() + * @method void setScopeCode(string $scopeCode) */ class Config extends \Magento\Framework\DataObject { @@ -87,6 +104,16 @@ class Config extends \Magento\Framework\DataObject */ private $settingChecker; + /** + * @var ScopeResolverPool + */ + private $scopeResolverPool; + + /** + * @var ScopeTypeNormalizer + */ + private $scopeTypeNormalizer; + /** * @param \Magento\Framework\App\Config\ReinitableConfigInterface $config * @param \Magento\Framework\Event\ManagerInterface $eventManager @@ -97,6 +124,9 @@ class Config extends \Magento\Framework\DataObject * @param \Magento\Store\Model\StoreManagerInterface $storeManager * @param Config\Reader\Source\Deployed\SettingChecker|null $settingChecker * @param array $data + * @param ScopeResolverPool|null $scopeResolverPool + * @param ScopeTypeNormalizer|null $scopeTypeNormalizer + * @SuppressWarnings(PHPMD.ExcessiveParameterList) */ public function __construct( \Magento\Framework\App\Config\ReinitableConfigInterface $config, @@ -107,7 +137,9 @@ public function __construct( \Magento\Framework\App\Config\ValueFactory $configValueFactory, \Magento\Store\Model\StoreManagerInterface $storeManager, SettingChecker $settingChecker = null, - array $data = [] + array $data = [], + ScopeResolverPool $scopeResolverPool = null, + ScopeTypeNormalizer $scopeTypeNormalizer = null ) { parent::__construct($data); $this->_eventManager = $eventManager; @@ -117,11 +149,17 @@ public function __construct( $this->_configLoader = $configLoader; $this->_configValueFactory = $configValueFactory; $this->_storeManager = $storeManager; - $this->settingChecker = $settingChecker ?: ObjectManager::getInstance()->get(SettingChecker::class); + $this->settingChecker = $settingChecker + ?? ObjectManager::getInstance()->get(SettingChecker::class); + $this->scopeResolverPool = $scopeResolverPool + ?? ObjectManager::getInstance()->get(ScopeResolverPool::class); + $this->scopeTypeNormalizer = $scopeTypeNormalizer + ?? ObjectManager::getInstance()->get(ScopeTypeNormalizer::class); } /** * Save config section + * * Require set: section, website, store and groups * * @throws \Exception @@ -504,8 +542,8 @@ public function setDataByPath($path, $value) } /** - * Get scope name and scopeId - * @todo refactor to scope resolver + * Set scope data + * * @return void */ private function initScope() @@ -513,31 +551,66 @@ private function initScope() if ($this->getSection() === null) { $this->setSection(''); } + + $scope = $this->retrieveScope(); + $this->setScope($this->scopeTypeNormalizer->normalize($scope->getScopeType())); + $this->setScopeCode($scope->getCode()); + $this->setScopeId($scope->getId()); + if ($this->getWebsite() === null) { - $this->setWebsite(''); + $this->setWebsite(StoreScopeInterface::SCOPE_WEBSITES === $this->getScope() ? $scope->getId() : ''); } if ($this->getStore() === null) { - $this->setStore(''); + $this->setStore(StoreScopeInterface::SCOPE_STORES === $this->getScope() ? $scope->getId() : ''); } + } - if ($this->getStore()) { - $scope = 'stores'; - $store = $this->_storeManager->getStore($this->getStore()); - $scopeId = (int)$store->getId(); - $scopeCode = $store->getCode(); - } elseif ($this->getWebsite()) { - $scope = 'websites'; - $website = $this->_storeManager->getWebsite($this->getWebsite()); - $scopeId = (int)$website->getId(); - $scopeCode = $website->getCode(); + /** + * Retrieve scope from initial data + * + * @return ScopeInterface + */ + private function retrieveScope(): ScopeInterface + { + $scopeType = $this->getScope(); + if (!$scopeType) { + switch (true) { + case $this->getStore(): + $scopeType = StoreScopeInterface::SCOPE_STORES; + $scopeIdentifier = $this->getStore(); + break; + case $this->getWebsite(): + $scopeType = StoreScopeInterface::SCOPE_WEBSITES; + $scopeIdentifier = $this->getWebsite(); + break; + default: + $scopeType = ScopeInterface::SCOPE_DEFAULT; + $scopeIdentifier = null; + break; + } } else { - $scope = 'default'; - $scopeId = 0; - $scopeCode = ''; + switch (true) { + case $this->getScopeId() !== null: + $scopeIdentifier = $this->getScopeId(); + break; + case $this->getScopeCode() !== null: + $scopeIdentifier = $this->getScopeCode(); + break; + case $this->getStore() !== null: + $scopeIdentifier = $this->getStore(); + break; + case $this->getWebsite() !== null: + $scopeIdentifier = $this->getWebsite(); + break; + default: + $scopeIdentifier = null; + break; + } } - $this->setScope($scope); - $this->setScopeId($scopeId); - $this->setScopeCode($scopeCode); + $scope = $this->scopeResolverPool->get($scopeType) + ->getScope($scopeIdentifier); + + return $scope; } /** diff --git a/app/code/Magento/Config/Model/Config/Backend/Currency/AbstractCurrency.php b/app/code/Magento/Config/Model/Config/Backend/Currency/AbstractCurrency.php index 4ae66bfd9692b..25303093ace5d 100644 --- a/app/code/Magento/Config/Model/Config/Backend/Currency/AbstractCurrency.php +++ b/app/code/Magento/Config/Model/Config/Backend/Currency/AbstractCurrency.php @@ -14,6 +14,8 @@ namespace Magento\Config\Model\Config\Backend\Currency; /** + * Base currency class + * * @api * @since 100.0.2 */ @@ -26,18 +28,19 @@ abstract class AbstractCurrency extends \Magento\Framework\App\Config\Value */ protected function _getAllowedCurrencies() { - if (!$this->isFormData() || $this->getData('groups/options/fields/allow/inherit')) { - return explode( + $allowValue = $this->getData('groups/options/fields/allow/value'); + $allowedCurrencies = $allowValue === null || $this->getData('groups/options/fields/allow/inherit') + ? explode( ',', (string)$this->_config->getValue( \Magento\Directory\Model\Currency::XML_PATH_CURRENCY_ALLOW, $this->getScope(), $this->getScopeId() ) - ); - } + ) + : (array) $allowValue; - return (array)$this->getData('groups/options/fields/allow/value'); + return $allowedCurrencies; } /** diff --git a/app/code/Magento/Config/Test/Mftf/Data/WebUrlOptionsConfigData.xml b/app/code/Magento/Config/Test/Mftf/Data/WebUrlOptionsConfigData.xml new file mode 100644 index 0000000000000..5010e383ba55c --- /dev/null +++ b/app/code/Magento/Config/Test/Mftf/Data/WebUrlOptionsConfigData.xml @@ -0,0 +1,24 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<entities xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:DataGenerator/etc/dataProfileSchema.xsd"> + <entity name="DefaultWebUrlOptionsConfig" type="web_url_use_store"> + <requiredEntity type="url_use_store_value">DefaultConfigWebUrlOptions</requiredEntity> + </entity> + <entity name="DefaultConfigWebUrlOptions" type="url_use_store_value"> + <data key="value">0</data> + </entity> + + <entity name="EnableWebUrlOptionsConfig" type="web_url_use_store"> + <requiredEntity type="url_use_store_value">WebUrlOptionsYes</requiredEntity> + </entity> + <entity name="WebUrlOptionsYes" type="url_use_store_value"> + <data key="value">1</data> + </entity> +</entities> \ No newline at end of file diff --git a/app/code/Magento/Config/Test/Mftf/Metadata/web_url_options_config-meta.xml b/app/code/Magento/Config/Test/Mftf/Metadata/web_url_options_config-meta.xml new file mode 100644 index 0000000000000..58809f8b41e28 --- /dev/null +++ b/app/code/Magento/Config/Test/Mftf/Metadata/web_url_options_config-meta.xml @@ -0,0 +1,22 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<operations xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:DataGenerator/etc/dataOperation.xsd"> + <operation name="WebUrlOptionsConfig" dataType="web_url_use_store" type="create" auth="adminFormKey" url="/admin/system_config/save/section/web/" + method="POST" successRegex="/messages-message-success/" returnRegex=""> + <object key="groups" dataType="web_url_use_store"> + <object key="url" dataType="web_url_use_store"> + <object key="fields" dataType="web_url_use_store"> + <object key="use_store" dataType="url_use_store_value"> + <field key="value">string</field> + </object> + </object> + </object> + </object> + </operation> +</operations> \ No newline at end of file diff --git a/app/code/Magento/Config/Test/Unit/Block/System/Config/FormTest.php b/app/code/Magento/Config/Test/Unit/Block/System/Config/FormTest.php index 83b7bd5fda42e..528d141306cce 100644 --- a/app/code/Magento/Config/Test/Unit/Block/System/Config/FormTest.php +++ b/app/code/Magento/Config/Test/Unit/Block/System/Config/FormTest.php @@ -103,6 +103,9 @@ protected function setUp() $this->_fieldsetFactoryMock = $this->createMock(\Magento\Config\Block\System\Config\Form\Fieldset\Factory::class); $this->_fieldFactoryMock = $this->createMock(\Magento\Config\Block\System\Config\Form\Field\Factory::class); $this->_coreConfigMock = $this->createMock(\Magento\Framework\App\Config\ScopeConfigInterface::class); + $settingCheckerMock = $this->getMockBuilder(SettingChecker::class) + ->disableOriginalConstructor() + ->getMock(); $this->_backendConfigMock = $this->createMock(\Magento\Config\Model\Config::class); @@ -150,6 +153,7 @@ protected function setUp() 'fieldsetFactory' => $this->_fieldsetFactoryMock, 'fieldFactory' => $this->_fieldFactoryMock, 'context' => $context, + 'settingChecker' => $settingCheckerMock, ]; $objectArguments = $helper->getConstructArguments(\Magento\Config\Block\System\Config\Form::class, $data); @@ -529,7 +533,7 @@ public function testInitFields( $elementVisibilityMock = $this->getMockBuilder(ElementVisibilityInterface::class) ->getMockForAbstractClass(); - $elementVisibilityMock->expects($this->once()) + $elementVisibilityMock->expects($this->any()) ->method('isDisabled') ->willReturn($isDisabled); diff --git a/app/code/Magento/Config/Test/Unit/Console/Command/ConfigSet/DefaultProcessorTest.php b/app/code/Magento/Config/Test/Unit/Console/Command/ConfigSet/DefaultProcessorTest.php index 984e0fe842687..3fb7d9ad21cd4 100644 --- a/app/code/Magento/Config/Test/Unit/Console/Command/ConfigSet/DefaultProcessorTest.php +++ b/app/code/Magento/Config/Test/Unit/Console/Command/ConfigSet/DefaultProcessorTest.php @@ -7,13 +7,14 @@ use Magento\Config\App\Config\Type\System; use Magento\Config\Console\Command\ConfigSet\DefaultProcessor; +use Magento\Config\Model\Config; +use Magento\Config\Model\Config\Factory as ConfigFactory; use Magento\Framework\App\Config\ConfigPathResolver; use Magento\Framework\App\Config\ScopeConfigInterface; use Magento\Framework\App\DeploymentConfig; use Magento\Store\Model\ScopeInterface; use Magento\Config\Model\PreparedValueFactory; use Magento\Framework\App\Config\Value; -use Magento\Framework\App\Config\ValueInterface; use Magento\Framework\Model\ResourceModel\Db\AbstractDb; use PHPUnit_Framework_MockObject_MockObject as Mock; @@ -55,17 +56,18 @@ class DefaultProcessorTest extends \PHPUnit\Framework\TestCase */ private $resourceModelMock; + /** + * @var ConfigFactory|Mock + */ + private $configFactory; + /** * @inheritdoc */ protected function setUp() { - $this->deploymentConfigMock = $this->getMockBuilder(DeploymentConfig::class) - ->disableOriginalConstructor() - ->getMock(); - $this->configPathResolverMock = $this->getMockBuilder(ConfigPathResolver::class) - ->disableOriginalConstructor() - ->getMock(); + $this->deploymentConfigMock = $this->createMock(DeploymentConfig::class); + $this->configPathResolverMock = $this->createMock(ConfigPathResolver::class); $this->resourceModelMock = $this->getMockBuilder(AbstractDb::class) ->disableOriginalConstructor() ->setMethods(['save']) @@ -74,14 +76,14 @@ protected function setUp() ->disableOriginalConstructor() ->setMethods(['getResource']) ->getMock(); - $this->preparedValueFactoryMock = $this->getMockBuilder(PreparedValueFactory::class) - ->disableOriginalConstructor() - ->getMock(); + $this->preparedValueFactoryMock = $this->createMock(PreparedValueFactory::class); + $this->configFactory = $this->createMock(ConfigFactory::class); $this->model = new DefaultProcessor( $this->preparedValueFactoryMock, $this->deploymentConfigMock, - $this->configPathResolverMock + $this->configPathResolverMock, + $this->configFactory ); } @@ -98,15 +100,14 @@ public function testProcess($path, $value, $scope, $scopeCode) { $this->configMockForProcessTest($path, $scope, $scopeCode); - $this->preparedValueFactoryMock->expects($this->once()) - ->method('create') - ->willReturn($this->valueMock); - $this->valueMock->expects($this->once()) - ->method('getResource') - ->willReturn($this->resourceModelMock); - $this->resourceModelMock->expects($this->once()) + $config = $this->createMock(Config::class); + $this->configFactory->method('create') + ->with(['scope' => $scope, 'scope_code' => $scopeCode]) + ->willReturn($config); + $config->method('setDataByPath') + ->with($path, $value); + $config->expects($this->once()) ->method('save') - ->with($this->valueMock) ->willReturnSelf(); $this->model->process($path, $value, $scope, $scopeCode); @@ -124,28 +125,6 @@ public function processDataProvider() ]; } - public function testProcessWithWrongValueInstance() - { - $path = 'test/test/test'; - $scope = ScopeConfigInterface::SCOPE_TYPE_DEFAULT; - $scopeCode = null; - $value = 'value'; - $valueInterfaceMock = $this->getMockBuilder(ValueInterface::class) - ->getMockForAbstractClass(); - - $this->configMockForProcessTest($path, $scope, $scopeCode); - - $this->preparedValueFactoryMock->expects($this->once()) - ->method('create') - ->willReturn($valueInterfaceMock); - $this->valueMock->expects($this->never()) - ->method('getResource'); - $this->resourceModelMock->expects($this->never()) - ->method('save'); - - $this->model->process($path, $value, $scope, $scopeCode); - } - /** * @param string $path * @param string $scope @@ -185,6 +164,9 @@ public function testProcessLockedValue() ->method('resolve') ->willReturn('system/default/test/test/test'); + $this->configFactory->expects($this->never()) + ->method('create'); + $this->model->process($path, $value, ScopeConfigInterface::SCOPE_TYPE_DEFAULT, null); } } diff --git a/app/code/Magento/Config/Test/Unit/Model/ConfigTest.php b/app/code/Magento/Config/Test/Unit/Model/ConfigTest.php index fcc1ff8b9c70c..bb772f51c0dac 100644 --- a/app/code/Magento/Config/Test/Unit/Model/ConfigTest.php +++ b/app/code/Magento/Config/Test/Unit/Model/ConfigTest.php @@ -5,138 +5,183 @@ */ namespace Magento\Config\Test\Unit\Model; +use PHPUnit_Framework_MockObject_MockObject as MockObject; +use Magento\Config\Model\Config; +use Magento\Framework\App\Config\ReinitableConfigInterface; +use Magento\Framework\Event\ManagerInterface; +use Magento\Config\Model\Config\Structure\Reader; +use Magento\Framework\DB\TransactionFactory; +use Magento\Config\Model\Config\Loader; +use Magento\Framework\App\Config\ValueFactory; +use Magento\Store\Model\StoreManagerInterface; +use Magento\Config\Model\Config\Structure; +use Magento\Config\Model\Config\Reader\Source\Deployed\SettingChecker; +use Magento\Framework\App\ScopeResolverPool; +use Magento\Framework\App\ScopeResolverInterface; +use Magento\Framework\App\ScopeInterface; +use Magento\Store\Model\ScopeTypeNormalizer; +use Magento\Framework\DB\Transaction; +use Magento\Framework\App\Config\Value; +use Magento\Store\Model\Website; +use Magento\Config\Model\Config\Structure\Element\Group; +use Magento\Config\Model\Config\Structure\Element\Field; + /** * @SuppressWarnings(PHPMD.CouplingBetweenObjects) */ class ConfigTest extends \PHPUnit\Framework\TestCase { /** - * @var \Magento\Config\Model\Config + * @var Config + */ + private $model; + + /** + * @var ManagerInterface|MockObject + */ + private $eventManagerMock; + + /** + * @var Reader|MockObject + */ + private $structureReaderMock; + + /** + * @var TransactionFactory|MockObject */ - protected $_model; + private $transFactoryMock; /** - * @var \PHPUnit_Framework_MockObject_MockObject + * @var ReinitableConfigInterface|MockObject */ - protected $_eventManagerMock; + private $appConfigMock; /** - * @var \PHPUnit_Framework_MockObject_MockObject + * @var Loader|MockObject */ - protected $_structureReaderMock; + private $configLoaderMock; /** - * @var \PHPUnit_Framework_MockObject_MockObject + * @var ValueFactory|MockObject */ - protected $_transFactoryMock; + private $dataFactoryMock; /** - * @var \Magento\Framework\App\Config\ReinitableConfigInterface|\PHPUnit_Framework_MockObject_MockObject + * @var StoreManagerInterface|MockObject */ - protected $_appConfigMock; + private $storeManager; /** - * @var \PHPUnit_Framework_MockObject_MockObject + * @var Structure|MockObject */ - protected $_applicationMock; + private $configStructure; /** - * @var \PHPUnit_Framework_MockObject_MockObject + * @var SettingChecker|MockObject */ - protected $_configLoaderMock; + private $settingsChecker; /** - * @var \PHPUnit_Framework_MockObject_MockObject + * @var ScopeResolverPool|MockObject */ - protected $_dataFactoryMock; + private $scopeResolverPool; /** - * @var \Magento\Store\Model\StoreManagerInterface + * @var ScopeResolverInterface|MockObject */ - protected $_storeManager; + private $scopeResolver; /** - * @var \Magento\Config\Model\Config\Structure + * @var ScopeInterface|MockObject */ - protected $_configStructure; + private $scope; /** - * @var \PHPUnit_Framework_MockObject_MockObject + * @var ScopeTypeNormalizer|MockObject */ - private $_settingsChecker; + private $scopeTypeNormalizer; protected function setUp() { - $this->_eventManagerMock = $this->createMock(\Magento\Framework\Event\ManagerInterface::class); - $this->_structureReaderMock = $this->createPartialMock( - \Magento\Config\Model\Config\Structure\Reader::class, + $this->eventManagerMock = $this->createMock(ManagerInterface::class); + $this->structureReaderMock = $this->createPartialMock( + Reader::class, ['getConfiguration'] ); - $this->_configStructure = $this->createMock(\Magento\Config\Model\Config\Structure::class); + $this->configStructure = $this->createMock(Structure::class); - $this->_structureReaderMock->expects( - $this->any() - )->method( - 'getConfiguration' - )->will( - $this->returnValue($this->_configStructure) - ); + $this->structureReaderMock->method('getConfiguration') + ->willReturn($this->configStructure); - $this->_transFactoryMock = $this->createPartialMock( - \Magento\Framework\DB\TransactionFactory::class, + $this->transFactoryMock = $this->createPartialMock( + TransactionFactory::class, ['create', 'addObject'] ); - $this->_appConfigMock = $this->createMock(\Magento\Framework\App\Config\ReinitableConfigInterface::class); - $this->_configLoaderMock = $this->createPartialMock( - \Magento\Config\Model\Config\Loader::class, + $this->appConfigMock = $this->createMock(ReinitableConfigInterface::class); + $this->configLoaderMock = $this->createPartialMock( + Loader::class, ['getConfigByPath'] ); - $this->_dataFactoryMock = $this->createMock(\Magento\Framework\App\Config\ValueFactory::class); - - $this->_storeManager = $this->getMockForAbstractClass(\Magento\Store\Model\StoreManagerInterface::class); - - $this->_settingsChecker = $this - ->createMock(\Magento\Config\Model\Config\Reader\Source\Deployed\SettingChecker::class); - - $this->_model = new \Magento\Config\Model\Config( - $this->_appConfigMock, - $this->_eventManagerMock, - $this->_configStructure, - $this->_transFactoryMock, - $this->_configLoaderMock, - $this->_dataFactoryMock, - $this->_storeManager, - $this->_settingsChecker + $this->dataFactoryMock = $this->createMock(ValueFactory::class); + + $this->storeManager = $this->createMock(StoreManagerInterface::class); + + $this->settingsChecker = $this->createMock(SettingChecker::class); + + $this->scopeResolverPool = $this->createMock(ScopeResolverPool::class); + $this->scopeResolver = $this->createMock(ScopeResolverInterface::class); + $this->scopeResolverPool->method('get') + ->willReturn($this->scopeResolver); + $this->scope = $this->createMock(ScopeInterface::class); + $this->scopeResolver->method('getScope') + ->willReturn($this->scope); + + $this->scopeTypeNormalizer = $this->createMock(ScopeTypeNormalizer::class); + + $this->model = new Config( + $this->appConfigMock, + $this->eventManagerMock, + $this->configStructure, + $this->transFactoryMock, + $this->configLoaderMock, + $this->dataFactoryMock, + $this->storeManager, + $this->settingsChecker, + [], + $this->scopeResolverPool, + $this->scopeTypeNormalizer ); } public function testSaveDoesNotDoAnythingIfGroupsAreNotPassed() { - $this->_configLoaderMock->expects($this->never())->method('getConfigByPath'); - $this->_model->save(); + $this->configLoaderMock->expects($this->never())->method('getConfigByPath'); + $this->model->save(); } public function testSaveEmptiesNonSetArguments() { - $this->_structureReaderMock->expects($this->never())->method('getConfiguration'); - $this->assertNull($this->_model->getSection()); - $this->assertNull($this->_model->getWebsite()); - $this->assertNull($this->_model->getStore()); - $this->_model->save(); - $this->assertSame('', $this->_model->getSection()); - $this->assertSame('', $this->_model->getWebsite()); - $this->assertSame('', $this->_model->getStore()); + $this->structureReaderMock->expects($this->never())->method('getConfiguration'); + $this->assertNull($this->model->getSection()); + $this->assertNull($this->model->getWebsite()); + $this->assertNull($this->model->getStore()); + $this->model->save(); + $this->assertSame('', $this->model->getSection()); + $this->assertSame('', $this->model->getWebsite()); + $this->assertSame('', $this->model->getStore()); } public function testSaveToCheckAdminSystemConfigChangedSectionEvent() { - $transactionMock = $this->createMock(\Magento\Framework\DB\Transaction::class); + $transactionMock = $this->createMock(Transaction::class); - $this->_transFactoryMock->expects($this->any())->method('create')->will($this->returnValue($transactionMock)); + $this->transFactoryMock->method('create') + ->willReturn($transactionMock); - $this->_configLoaderMock->expects($this->any())->method('getConfigByPath')->will($this->returnValue([])); + $this->configLoaderMock->method('getConfigByPath') + ->willReturn([]); - $this->_eventManagerMock->expects( + $this->eventManagerMock->expects( $this->at(0) )->method( 'dispatch' @@ -145,7 +190,7 @@ public function testSaveToCheckAdminSystemConfigChangedSectionEvent() $this->arrayHasKey('website') ); - $this->_eventManagerMock->expects( + $this->eventManagerMock->expects( $this->at(0) )->method( 'dispatch' @@ -154,123 +199,147 @@ public function testSaveToCheckAdminSystemConfigChangedSectionEvent() $this->arrayHasKey('store') ); - $this->_model->setGroups(['1' => ['data']]); - $this->_model->save(); + $this->model->setGroups(['1' => ['data']]); + $this->model->save(); } public function testDoNotSaveReadOnlyFields() { - $transactionMock = $this->createMock(\Magento\Framework\DB\Transaction::class); - $this->_transFactoryMock->expects($this->any())->method('create')->will($this->returnValue($transactionMock)); + $transactionMock = $this->createMock(Transaction::class); + $this->transFactoryMock->method('create') + ->willReturn($transactionMock); - $this->_settingsChecker->expects($this->any())->method('isReadOnly')->will($this->returnValue(true)); - $this->_configLoaderMock->expects($this->any())->method('getConfigByPath')->will($this->returnValue([])); + $this->settingsChecker->method('isReadOnly') + ->willReturn(true); + $this->configLoaderMock->method('getConfigByPath') + ->willReturn([]); - $this->_model->setGroups(['1' => ['fields' => ['key' => ['data']]]]); - $this->_model->setSection('section'); + $this->model->setGroups(['1' => ['fields' => ['key' => ['data']]]]); + $this->model->setSection('section'); - $group = $this->createMock(\Magento\Config\Model\Config\Structure\Element\Group::class); - $group->method('getPath')->willReturn('section/1'); + $group = $this->createMock(Group::class); + $group->method('getPath') + ->willReturn('section/1'); - $field = $this->createMock(\Magento\Config\Model\Config\Structure\Element\Field::class); - $field->method('getGroupPath')->willReturn('section/1'); - $field->method('getId')->willReturn('key'); + $field = $this->createMock(Field::class); + $field->method('getGroupPath') + ->willReturn('section/1'); + $field->method('getId') + ->willReturn('key'); - $this->_configStructure->expects($this->at(0)) + $this->configStructure->expects($this->at(0)) ->method('getElement') ->with('section/1') - ->will($this->returnValue($group)); - $this->_configStructure->expects($this->at(1)) + ->willReturn($group); + $this->configStructure->expects($this->at(1)) ->method('getElement') ->with('section/1') - ->will($this->returnValue($group)); - $this->_configStructure->expects($this->at(2)) + ->willReturn($group); + $this->configStructure->expects($this->at(2)) ->method('getElement') ->with('section/1/key') - ->will($this->returnValue($field)); + ->willReturn($field); $backendModel = $this->createPartialMock( - \Magento\Framework\App\Config\Value::class, + Value::class, ['addData'] ); - $this->_dataFactoryMock->expects($this->any())->method('create')->will($this->returnValue($backendModel)); + $this->dataFactoryMock->method('create') + ->willReturn($backendModel); - $this->_transFactoryMock->expects($this->never())->method('addObject'); - $backendModel->expects($this->never())->method('addData'); + $this->transFactoryMock->expects($this->never()) + ->method('addObject'); + $backendModel->expects($this->never()) + ->method('addData'); - $this->_model->save(); + $this->model->save(); } public function testSaveToCheckScopeDataSet() { - $transactionMock = $this->createMock(\Magento\Framework\DB\Transaction::class); - $this->_transFactoryMock->expects($this->any())->method('create')->will($this->returnValue($transactionMock)); + $transactionMock = $this->createMock(Transaction::class); + $this->transFactoryMock->method('create') + ->willReturn($transactionMock); - $this->_configLoaderMock->expects($this->any())->method('getConfigByPath')->will($this->returnValue([])); + $this->configLoaderMock->method('getConfigByPath') + ->willReturn([]); - $this->_eventManagerMock->expects($this->at(0)) + $this->eventManagerMock->expects($this->at(0)) ->method('dispatch') ->with( $this->equalTo('admin_system_config_changed_section_section'), $this->arrayHasKey('website') ); - $this->_eventManagerMock->expects($this->at(0)) + $this->eventManagerMock->expects($this->at(0)) ->method('dispatch') ->with( $this->equalTo('admin_system_config_changed_section_section'), $this->arrayHasKey('store') ); - $group = $this->createMock(\Magento\Config\Model\Config\Structure\Element\Group::class); + $group = $this->createMock(Group::class); $group->method('getPath')->willReturn('section/1'); - $field = $this->createMock(\Magento\Config\Model\Config\Structure\Element\Field::class); + $field = $this->createMock(Field::class); $field->method('getGroupPath')->willReturn('section/1'); $field->method('getId')->willReturn('key'); - $this->_configStructure->expects($this->at(0)) + $this->configStructure->expects($this->at(0)) ->method('getElement') ->with('section/1') - ->will($this->returnValue($group)); - $this->_configStructure->expects($this->at(1)) + ->willReturn($group); + $this->configStructure->expects($this->at(1)) ->method('getElement') ->with('section/1') - ->will($this->returnValue($group)); - $this->_configStructure->expects($this->at(2)) + ->willReturn($group); + $this->configStructure->expects($this->at(2)) ->method('getElement') ->with('section/1/key') - ->will($this->returnValue($field)); - $this->_configStructure->expects($this->at(3)) + ->willReturn($field); + $this->configStructure->expects($this->at(3)) ->method('getElement') ->with('section/1') - ->will($this->returnValue($group)); - $this->_configStructure->expects($this->at(4)) + ->willReturn($group); + $this->configStructure->expects($this->at(4)) ->method('getElement') ->with('section/1/key') - ->will($this->returnValue($field)); - - $website = $this->createMock(\Magento\Store\Model\Website::class); - $website->expects($this->any())->method('getCode')->will($this->returnValue('website_code')); - $this->_storeManager->expects($this->any())->method('getWebsite')->will($this->returnValue($website)); - $this->_storeManager->expects($this->any())->method('getWebsites')->will($this->returnValue([$website])); - $this->_storeManager->expects($this->any())->method('isSingleStoreMode')->will($this->returnValue(true)); - - $this->_model->setWebsite('website'); - $this->_model->setSection('section'); - $this->_model->setGroups(['1' => ['fields' => ['key' => ['data']]]]); + ->willReturn($field); + + $this->scopeResolver->method('getScope') + ->with('1') + ->willReturn($this->scope); + $this->scope->expects($this->atLeastOnce()) + ->method('getScopeType') + ->willReturn('website'); + $this->scope->expects($this->atLeastOnce()) + ->method('getId') + ->willReturn(1); + $this->scope->expects($this->atLeastOnce()) + ->method('getCode') + ->willReturn('website_code'); + $this->scopeTypeNormalizer->expects($this->atLeastOnce()) + ->method('normalize') + ->with('website') + ->willReturn('websites'); + $website = $this->createMock(Website::class); + $this->storeManager->method('getWebsites')->willReturn([$website]); + $this->storeManager->method('isSingleStoreMode')->willReturn(true); + + $this->model->setWebsite('1'); + $this->model->setSection('section'); + $this->model->setGroups(['1' => ['fields' => ['key' => ['data']]]]); $backendModel = $this->createPartialMock( - \Magento\Framework\App\Config\Value::class, + Value::class, ['setPath', 'addData', '__sleep', '__wakeup'] ); - $backendModel->expects($this->once()) - ->method('addData') + $backendModel->method('addData') ->with([ 'field' => 'key', 'groups' => [1 => ['fields' => ['key' => ['data']]]], 'group_id' => null, 'scope' => 'websites', - 'scope_id' => 0, + 'scope_id' => 1, 'scope_code' => 'website_code', 'field_config' => null, 'fieldset_data' => ['key' => null], @@ -278,18 +347,19 @@ public function testSaveToCheckScopeDataSet() $backendModel->expects($this->once()) ->method('setPath') ->with('section/1/key') - ->will($this->returnValue($backendModel)); + ->willReturn($backendModel); - $this->_dataFactoryMock->expects($this->any())->method('create')->will($this->returnValue($backendModel)); + $this->dataFactoryMock->method('create') + ->willReturn($backendModel); - $this->_model->save(); + $this->model->save(); } public function testSetDataByPath() { $value = 'value'; $path = '<section>/<group>/<field>'; - $this->_model->setDataByPath($path, $value); + $this->model->setDataByPath($path, $value); $expected = [ 'section' => '<section>', 'groups' => [ @@ -300,7 +370,7 @@ public function testSetDataByPath() ], ], ]; - $this->assertSame($expected, $this->_model->getData()); + $this->assertSame($expected, $this->model->getData()); } /** @@ -309,7 +379,7 @@ public function testSetDataByPath() */ public function testSetDataByPathEmpty() { - $this->_model->setDataByPath('', 'value'); + $this->model->setDataByPath('', 'value'); } /** @@ -324,7 +394,7 @@ public function testSetDataByPathWrongDepth($path, $expectedException) $this->expectException('\UnexpectedValueException'); $this->expectExceptionMessage($expectedException); $value = 'value'; - $this->_model->setDataByPath($path, $value); + $this->model->setDataByPath($path, $value); } /** diff --git a/app/code/Magento/Config/etc/module.xml b/app/code/Magento/Config/etc/module.xml index cdf31ab7a5d19..b7df33554af90 100644 --- a/app/code/Magento/Config/etc/module.xml +++ b/app/code/Magento/Config/etc/module.xml @@ -6,7 +6,7 @@ */ --> <config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:framework:Module/etc/module.xsd"> - <module name="Magento_Config" setup_version="2.0.0"> + <module name="Magento_Config" setup_version="2.1.0"> <sequence> <module name="Magento_Store"/> </sequence> diff --git a/app/code/Magento/ConfigurableImportExport/Model/Export/RowCustomizer.php b/app/code/Magento/ConfigurableImportExport/Model/Export/RowCustomizer.php index 58462b873d8b1..7146108f61fe1 100644 --- a/app/code/Magento/ConfigurableImportExport/Model/Export/RowCustomizer.php +++ b/app/code/Magento/ConfigurableImportExport/Model/Export/RowCustomizer.php @@ -3,14 +3,21 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ +declare(strict_types=1); + namespace Magento\ConfigurableImportExport\Model\Export; -use Magento\CatalogImportExport\Model\Export\RowCustomizerInterface; use Magento\Catalog\Model\ResourceModel\Product\Collection as ProductCollection; -use Magento\ConfigurableProduct\Model\Product\Type\Configurable as ConfigurableProductType; +use Magento\CatalogImportExport\Model\Export\RowCustomizerInterface; use Magento\CatalogImportExport\Model\Import\Product as ImportProduct; +use Magento\ConfigurableProduct\Model\Product\Type\Configurable as ConfigurableProductType; use Magento\ImportExport\Model\Import; +use Magento\Store\Model\Store; +use Magento\Store\Model\StoreManagerInterface; +/** + * Customizes output during export + */ class RowCustomizer implements RowCustomizerInterface { /** @@ -36,6 +43,19 @@ class RowCustomizer implements RowCustomizerInterface self::CONFIGURABLE_VARIATIONS_LABELS_COLUMN ]; + /** + * @var StoreManagerInterface + */ + private $storeManager; + + /** + * @param StoreManagerInterface $storeManager + */ + public function __construct(StoreManagerInterface $storeManager) + { + $this->storeManager = $storeManager; + } + /** * Prepare configurable data for export * @@ -49,6 +69,9 @@ public function prepareData($collection, $productIds) $productCollection->addAttributeToFilter('entity_id', ['in' => $productIds]) ->addAttributeToFilter('type_id', ['eq' => ConfigurableProductType::TYPE_CODE]); + // set global scope during export + $this->storeManager->setCurrentStore(Store::DEFAULT_STORE_ID); + while ($product = $productCollection->fetchItem()) { $productAttributesOptions = $product->getTypeInstance()->getConfigurableOptions($product); $this->configurableData[$product->getId()] = []; diff --git a/app/code/Magento/ConfigurableImportExport/composer.json b/app/code/Magento/ConfigurableImportExport/composer.json index 253d2b39b9881..b3d8af9f419d2 100644 --- a/app/code/Magento/ConfigurableImportExport/composer.json +++ b/app/code/Magento/ConfigurableImportExport/composer.json @@ -8,7 +8,8 @@ "magento/module-eav": "101.0.*", "magento/module-import-export": "100.2.*", "magento/module-configurable-product": "100.2.*", - "magento/framework": "101.0.*" + "magento/framework": "101.0.*", + "magento/module-store": "100.2.*" }, "type": "magento2-module", "version": "100.2.4", 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 4c7c5df736112..b128458665d73 100644 --- a/app/code/Magento/ConfigurableProduct/Block/Cart/Item/Renderer/Configurable.php +++ b/app/code/Magento/ConfigurableProduct/Block/Cart/Item/Renderer/Configurable.php @@ -70,4 +70,12 @@ public function getIdentities() } return $identities; } + + /** + * @inheritdoc + */ + public function getProductPriceHtml(\Magento\Catalog\Model\Product $product) + { + return parent::getProductPriceHtml($this->getChildProduct()); + } } 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 81e2e99bfe93a..b7bbf7aa1871c 100644 --- a/app/code/Magento/ConfigurableProduct/Model/ResourceModel/Product/Indexer/Price/Configurable.php +++ b/app/code/Magento/ConfigurableProduct/Model/ResourceModel/Product/Indexer/Price/Configurable.php @@ -12,6 +12,11 @@ use Magento\Catalog\Model\ResourceModel\Product\Indexer\Price\Query\BaseFinalPrice; use Magento\Catalog\Model\ResourceModel\Product\Indexer\Price\IndexTableStructureFactory; use Magento\Catalog\Model\ResourceModel\Product\Indexer\Price\IndexTableStructure; +use Magento\Framework\App\Config\ScopeConfigInterface; +use Magento\Store\Model\ScopeInterface; +use Magento\Framework\App\ObjectManager; +use Magento\CatalogInventory\Model\Stock; +use Magento\CatalogInventory\Model\Configuration; /** * Configurable Products Price Indexer Resource model @@ -65,6 +70,11 @@ class Configurable implements DimensionalIndexerInterface */ private $basePriceModifier; + /** + * @var ScopeConfigInterface + */ + private $scopeConfig; + /** * @param BaseFinalPrice $baseFinalPrice * @param IndexTableStructureFactory $indexTableStructureFactory @@ -74,6 +84,7 @@ class Configurable implements DimensionalIndexerInterface * @param BasePriceModifier $basePriceModifier * @param bool $fullReindexAction * @param string $connectionName + * @param ScopeConfigInterface $scopeConfig */ public function __construct( BaseFinalPrice $baseFinalPrice, @@ -83,7 +94,8 @@ public function __construct( \Magento\Framework\App\ResourceConnection $resource, BasePriceModifier $basePriceModifier, $fullReindexAction = false, - $connectionName = 'indexer' + $connectionName = 'indexer', + ScopeConfigInterface $scopeConfig = null ) { $this->baseFinalPrice = $baseFinalPrice; $this->indexTableStructureFactory = $indexTableStructureFactory; @@ -93,10 +105,11 @@ public function __construct( $this->resource = $resource; $this->fullReindexAction = $fullReindexAction; $this->basePriceModifier = $basePriceModifier; + $this->scopeConfig = $scopeConfig ?: ObjectManager::getInstance()->get(ScopeConfigInterface::class); } /** - * {@inheritdoc} + * @inheritdoc * * @throws \Exception */ @@ -184,7 +197,19 @@ private function fillTemporaryOptionsTable(string $temporaryOptionsTableName, ar ['le' => $this->getTable('catalog_product_entity')], 'le.' . $linkField . ' = l.parent_id', [] - )->columns( + ); + + // Does not make sense to extend query if out of stock products won't appear in tables for indexing + if ($this->isConfigShowOutOfStock()) { + $select->join( + ['si' => $this->getTable('cataloginventory_stock_item')], + 'si.product_id = l.product_id', + [] + ); + $select->where('si.is_in_stock = ?', Stock::STOCK_IN_STOCK); + } + + $select->columns( [ 'le.entity_id', 'customer_group_id', @@ -250,7 +275,7 @@ private function getMainTable($dimensions) /** * Get connection * - * return \Magento\Framework\DB\Adapter\AdapterInterface + * @return \Magento\Framework\DB\Adapter\AdapterInterface * @throws \DomainException */ private function getConnection(): \Magento\Framework\DB\Adapter\AdapterInterface @@ -272,4 +297,17 @@ private function getTable($tableName) { return $this->resource->getTableName($tableName, $this->connectionName); } + + /** + * Is flag Show Out Of Stock setted + * + * @return bool + */ + private function isConfigShowOutOfStock(): bool + { + return $this->scopeConfig->isSetFlag( + Configuration::XML_PATH_SHOW_OUT_OF_STOCK, + ScopeInterface::SCOPE_STORE + ); + } } diff --git a/app/code/Magento/ConfigurableProduct/Plugin/Catalog/Model/Product/Pricing/Renderer/SalableResolver.php b/app/code/Magento/ConfigurableProduct/Plugin/Catalog/Model/Product/Pricing/Renderer/SalableResolver.php index 8a7e846c0e9f1..8c80a56a649e7 100644 --- a/app/code/Magento/ConfigurableProduct/Plugin/Catalog/Model/Product/Pricing/Renderer/SalableResolver.php +++ b/app/code/Magento/ConfigurableProduct/Plugin/Catalog/Model/Product/Pricing/Renderer/SalableResolver.php @@ -5,7 +5,7 @@ */ namespace Magento\ConfigurableProduct\Plugin\Catalog\Model\Product\Pricing\Renderer; -use Magento\ConfigurableProduct\Pricing\Price\LowestPriceOptionsProviderInterface; +use Magento\ConfigurableProduct\Model\Product\Type\Configurable as TypeConfigurable; /** * A plugin for a salable resolver. @@ -13,17 +13,16 @@ class SalableResolver { /** - * @var LowestPriceOptionsProviderInterface + * @var TypeConfigurable */ - private $lowestPriceOptionsProvider; + private $typeConfigurable; /** - * @param LowestPriceOptionsProviderInterface $lowestPriceOptionsProvider + * @param TypeConfigurable $typeConfigurable */ - public function __construct( - LowestPriceOptionsProviderInterface $lowestPriceOptionsProvider - ) { - $this->lowestPriceOptionsProvider = $lowestPriceOptionsProvider; + public function __construct(TypeConfigurable $typeConfigurable) + { + $this->typeConfigurable = $typeConfigurable; } /** @@ -33,9 +32,7 @@ public function __construct( * @param \Magento\Catalog\Model\Product\Pricing\Renderer\SalableResolver $subject * @param bool $result * @param \Magento\Framework\Pricing\SaleableInterface $salableItem - * * @return bool - * * @SuppressWarnings(PHPMD.UnusedFormalParameter) */ public function afterIsSalable( @@ -43,8 +40,8 @@ public function afterIsSalable( $result, \Magento\Framework\Pricing\SaleableInterface $salableItem ) { - if ($salableItem->getTypeId() == 'configurable' && $result) { - $result = $salableItem->isSalable(); + if ($salableItem->getTypeId() === TypeConfigurable::TYPE_CODE && $result) { + $result = $this->typeConfigurable->isSalable($salableItem); } return $result; diff --git a/app/code/Magento/ConfigurableProduct/Test/Mftf/ActionGroup/AdminCreateConfigurableProductActionGroup.xml b/app/code/Magento/ConfigurableProduct/Test/Mftf/ActionGroup/AdminCreateConfigurableProductActionGroup.xml index 3b4e7f55d186c..e16ee43978a1e 100644 --- a/app/code/Magento/ConfigurableProduct/Test/Mftf/ActionGroup/AdminCreateConfigurableProductActionGroup.xml +++ b/app/code/Magento/ConfigurableProduct/Test/Mftf/ActionGroup/AdminCreateConfigurableProductActionGroup.xml @@ -65,4 +65,29 @@ <conditionalClick selector="{{AdminChooseAffectedAttributeSetPopup.confirm}}" dependentSelector="{{AdminChooseAffectedAttributeSetPopup.confirm}}" visible="true" stepKey="clickConfirm"/> <see selector="{{AdminProductMessagesSection.successMessage}}" userInput="You saved the product." stepKey="assertSuccess"/> </actionGroup> + + <actionGroup name="AdminGenerateProductConfigurations"> + <arguments> + <argument name="attributeCode" type="string"/> + <argument name="qty" type="string"/> + <argument name="price" type="string"/> + </arguments> + <conditionalClick selector="{{AdminDataGridHeaderSection.clearFilters}}" dependentSelector="{{AdminDataGridHeaderSection.clearFilters}}" visible="true" stepKey="clearExistingFilters"/> + <click selector="{{AdminDataGridHeaderSection.filters}}" stepKey="clickOnFilters"/> + <fillField selector="{{AdminProductAttributeGridSection.attributeCodeFilterInput}}" userInput="{{attributeCode}}" stepKey="fillFilterAttributeCodeField"/> + <click selector="{{AdminDataGridHeaderSection.applyFilters}}" stepKey="clickApplyFiltersButton"/> + <click selector="{{AdminDataGridTableSection.rowCheckbox('1')}}" stepKey="clickOnFirstCheckbox"/> + <click selector="{{AdminCreateProductConfigurationsPanel.next}}" stepKey="clickNextStep"/> + <waitForElementVisible selector="{{AdminCreateProductConfigurationsPanelSection.selectAllByAttribute(attributeCode)}}" stepKey="waitForNextPageOpened"/> + <click selector="{{AdminCreateProductConfigurationsPanelSection.selectAllByAttribute(attributeCode)}}" stepKey="clickSelectAll"/> + <click selector="{{AdminCreateProductConfigurationsPanel.next}}" stepKey="clickNextStep1"/> + <waitForElementVisible selector="{{AdminCreateProductConfigurationsPanel.applySinglePriceToAllSkus}}" stepKey="waitForNextPageOpened1"/> + <click selector="{{AdminCreateProductConfigurationsPanel.applySinglePriceToAllSkus}}" stepKey="clickOnApplySinglePriceToAllSkus"/> + <fillField selector="{{AdminCreateProductConfigurationsPanel.singlePrice}}" userInput="{{price}}" stepKey="enterAttributePrice"/> + <click selector="{{AdminCreateProductConfigurationsPanel.applySingleQuantityToEachSkus}}" stepKey="clickOnApplySingleQuantityToEachSku"/> + <fillField selector="{{AdminCreateProductConfigurationsPanel.quantity}}" userInput="{{qty}}" stepKey="enterAttributeQuantity"/> + <click selector="{{AdminCreateProductConfigurationsPanel.next}}" stepKey="clickNextStep2"/> + <waitForElementVisible selector="{{AdminCreateProductConfigurationsPanel.next}}" stepKey="waitForNextPageOpened2"/> + <click selector="{{AdminCreateProductConfigurationsPanel.next}}" stepKey="generateProducts"/> + </actionGroup> </actionGroups> diff --git a/app/code/Magento/ConfigurableProduct/Test/Mftf/Section/AdminProductFormConfigurationsSection.xml b/app/code/Magento/ConfigurableProduct/Test/Mftf/Section/AdminProductFormConfigurationsSection.xml index 5ae70488d164d..c774dad1ed607 100644 --- a/app/code/Magento/ConfigurableProduct/Test/Mftf/Section/AdminProductFormConfigurationsSection.xml +++ b/app/code/Magento/ConfigurableProduct/Test/Mftf/Section/AdminProductFormConfigurationsSection.xml @@ -7,7 +7,7 @@ --> <sections xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" - xsi:noNamespaceSchemaLocation="../../../../../../../dev/tests/acceptance/vendor/magento/magento2-functional-testing-framework/src/Magento/FunctionalTestingFramework/Page/etc/SectionObject.xsd"> + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Page/etc/SectionObject.xsd"> <section name="AdminProductFormConfigurationsSection"> <element name="sectionHeader" type="text" selector=".admin__collapsible-block-wrapper[data-index='configurable']"/> <element name="createConfigurations" type="button" selector="button[data-index='create_configurable_products_button']" timeout="30"/> @@ -22,5 +22,7 @@ <element name="removeProductBtn" type="button" selector="//a[text()='Remove Product']"/> <element name="disableProductBtn" type="button" selector="//a[text()='Disable Product']"/> <element name="enableProductBtn" type="button" selector="//a[text()='Enable Product']"/> + <element name="configurableMatrixSku" type="input" selector="input[name='configurable-matrix[{{index}}][sku]']" parameterized="true"/> + <element name="skuValidationMessage" type="text" selector="input[name='configurable-matrix[{{index}}][sku]'] + label" parameterized="true"/> </section> </sections> diff --git a/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdminCheckValidatorConfigurableProductTest.xml b/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdminCheckValidatorConfigurableProductTest.xml new file mode 100644 index 0000000000000..03f4d8461cebb --- /dev/null +++ b/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdminCheckValidatorConfigurableProductTest.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="AdminCheckValidatorConfigurableProductTest"> + <annotations> + <features value="ConfigurableProduct"/> + <stories value="Create a Configurable Product via the Admin"/> + <title value="Check that validator works correctly when creating Configurations for Configurable Products"/> + <description value="Verify validator works correctly for Configurable Products"/> + <severity value="MAJOR"/> + <testCaseId value="MC-13719"/> + <group value="configurableProduct"/> + </annotations> + + <before> + <!--Login as admin--> + <actionGroup ref="LoginAsAdmin" stepKey="loginAsAdmin"/> + <!--Create Category--> + <createData entity="ApiCategory" stepKey="createCategory"/> + <!--Create Configurable product--> + <createData entity="ApiConfigurableProduct" stepKey="createConfigProduct"> + <requiredEntity createDataKey="createCategory"/> + </createData> + </before> + + <after> + <!--Delete created data--> + <deleteData createDataKey="createCategory" stepKey="deleteCategory"/> + <actionGroup ref="deleteProductUsingProductGrid" stepKey="deleteProduct"> + <argument name="product" value="ApiConfigurableProduct"/> + <argument name="productCount" value="2"/> + </actionGroup> + + <amOnPage url="{{AdminProductIndexPage.url}}" stepKey="openAdminProductPage"/> + <actionGroup ref="clearFiltersAdminDataGrid" stepKey="resetProductsFilter" /> + + <!-- Remove attribute --> + <actionGroup ref="deleteProductAttribute" stepKey="deleteAttribute"> + <argument name="ProductAttribute" value="productAttributeWithDropdownTwoOptions"/> + </actionGroup> + <amOnPage url="{{AdminProductAttributeGridPage.url}}" stepKey="goToProductAttributesGridPage"/> + <actionGroup ref="clearFiltersAdminDataGrid" stepKey="resetAttributesFilter" /> + <actionGroup ref="logout" stepKey="logout"/> + </after> + + <!-- Find the product that we just created using the product grid --> + <amOnPage url="{{AdminProductIndexPage.url}}" stepKey="visitAdminProductPage"/> + <actionGroup ref="filterProductGridBySku" stepKey="findCreatedProduct"> + <argument name="product" value="ApiConfigurableProduct"/> + </actionGroup> + <click selector="{{AdminProductGridSection.firstRow}}" stepKey="clickOnProduct"/> + <waitForPageLoad stepKey="waitForProductPageLoad"/> + + <!-- Create configurations for product we created earlier --> + <click selector="{{AdminProductFormConfigurationsSection.createConfigurations}}" stepKey="clickCreateConfigurations"/> + + <!--Create new attribute--> + <waitForElementVisible selector="{{AdminCreateProductConfigurationsPanel.createNewAttribute}}" stepKey="waitForNewAttributePageOpened"/> + <click selector="{{AdminCreateProductConfigurationsPanel.createNewAttribute}}" stepKey="clickCreateNewAttribute"/> + <switchToIFrame selector="{{AdminNewAttributePanelSection.newAttributeIFrame}}" stepKey="enterAttributePanelIFrame"/> + <waitForElementVisible selector="{{AdminNewAttributePanel.defaultLabel}}" time="30" stepKey="waitForIframeLoad"/> + <fillField selector="{{AdminNewAttributePanel.defaultLabel}}" userInput="{{productAttributeWithDropdownTwoOptions.attribute_code}}" stepKey="fillDefaultLabel"/> + <selectOption selector="{{AdminNewAttributePanelSection.inputType}}" userInput="{{colorProductAttribute.input_type}}" stepKey="selectAttributeInputType"/> + <!--Add option to attribute--> + <click selector="{{AdminNewAttributePanelSection.addOption}}" stepKey="clickAddOption"/> + <waitForElementVisible selector="{{AdminNewAttributePanelSection.isDefault('1')}}" time="30" stepKey="waitForOptionRow"/> + <fillField selector="{{AdminNewAttributePanelSection.optionAdminValue('0')}}" userInput="ThisIsLongNameNameLengthMoreThanSixtyFourThisIsLongNameNameLength" stepKey="fillAdminLabel"/> + <fillField selector="{{AdminNewAttributePanelSection.optionDefaultStoreValue('0')}}" userInput="{{colorProductAttribute1.name}}" stepKey="fillDefaultLabel1"/> + + <!--Save attribute--> + <click selector="{{AdminNewAttributePanelSection.saveAttribute}}" stepKey="clickOnNewAttributePanel"/> + <waitForPageLoad stepKey="waitForSaveAttribute"/> + <switchToIFrame stepKey="switchOutOfIFrame"/> + + <!-- Generate products --> + <actionGroup ref="AdminGenerateProductConfigurations" stepKey="generateProducts"> + <argument name="attributeCode" value="{{productAttributeWithDropdownTwoOptions.attribute_code}}"/> + <argument name="qty" value="100"/> + <argument name="price" value="10"/> + </actionGroup> + <waitForElementVisible selector="{{AdminProductFormActionSection.saveButton}}" stepKey="waitForSaveButtonVisible"/> + <scrollToTopOfPage stepKey="scrollToTop"/> + <click selector="{{AdminProductFormActionSection.saveButton}}" stepKey="saveProduct"/> + <waitForElementVisible selector="{{AdminChooseAffectedAttributeSetPopup.confirm}}" stepKey="waitForPopUpVisible"/> + <click selector="{{AdminChooseAffectedAttributeSetPopup.confirm}}" stepKey="clickOnConfirmInPopup"/> + <dontSeeElement selector="{{AdminMessagesSection.success}}" stepKey="dontSeeSaveProductMessage"/> + + <!--Close modal window--> + <click selector="{{AdminChooseAffectedAttributeSetPopup.closePopUp}}" stepKey="clickOnClosePopup"/> + <waitForElementNotVisible selector="{{AdminChooseAffectedAttributeSetPopup.closePopUp}}" stepKey="waitForDialogClosed"/> + + <!--See that validation message is shown under the fields--> + <scrollTo selector="{{AdminProductFormConfigurationsSection.currentVariationsSkuCells}}" stepKey="scrollTConfigurationTab"/> + <see userInput="Please enter less or equal than 64 symbols." selector="{{AdminProductFormConfigurationsSection.skuValidationMessage('0')}}" stepKey="seeValidationMessage"/> + + <!--Edit "SKU" with valid quantity--> + <fillField selector="{{AdminProductFormConfigurationsSection.configurableMatrixSku('0')}}" userInput="{{ApiConfigurableProduct.sku}}-thisIsShortName" stepKey="fillValidValue"/> + + <!--Click on "Save"--> + <waitForElementVisible selector="{{AdminProductFormActionSection.saveButton}}" stepKey="waitForSaveBtnVisible"/> + <scrollToTopOfPage stepKey="scrollToTop1"/> + <click selector="{{AdminProductFormActionSection.saveButton}}" stepKey="saveProductAgain"/> + + <!--Click on "Confirm". Product is saved, success message appears --> + <waitForElementVisible selector="{{AdminChooseAffectedAttributeSetPopup.confirm}}" stepKey="waitPopUpVisible"/> + <click selector="{{AdminChooseAffectedAttributeSetPopup.confirm}}" stepKey="clickOnConfirmPopup"/> + <see selector="{{AdminMessagesSection.success}}" userInput="You saved the product." stepKey="seeSaveProductMessage"/> + </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 a1e3e0b83e722..36615d3af6b7b 100644 --- a/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/StorefrontConfigurableProductWithFileCustomOptionTest.xml +++ b/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/StorefrontConfigurableProductWithFileCustomOptionTest.xml @@ -60,7 +60,6 @@ <!--Try invalid file--> <attachFile selector="{{StorefrontProductInfoMainSection.addLinkFileUploadFile(ProductOptionFile.title)}}" userInput="lorem_ipsum.docx" stepKey="attachInvalidFile"/> <click selector="{{StorefrontProductPageSection.addToCartBtn}}" stepKey="addToCartInvalidFile"/> - <waitForPageLoad time="30" stepKey="waitForAddToCartWithError"/> <waitForElementVisible selector="{{StorefrontProductPageSection.alertMessage}}" stepKey="waitForErrorMessageInvalidFile"/> <see selector="{{StorefrontProductPageSection.messagesBlock}}" userInput="The file 'lorem_ipsum.docx' for '{{ProductOptionFile.title}}' has an invalid extension." stepKey="seeMessageInvalidFile"/> <!--Option remains selected--> diff --git a/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/StorefrontSortingByPriceForConfigurableWithCatalogRuleAppliedTest.xml b/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/StorefrontSortingByPriceForConfigurableWithCatalogRuleAppliedTest.xml index d07ff3ec4d656..c8ca2ced0d60f 100644 --- a/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/StorefrontSortingByPriceForConfigurableWithCatalogRuleAppliedTest.xml +++ b/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/StorefrontSortingByPriceForConfigurableWithCatalogRuleAppliedTest.xml @@ -16,9 +16,6 @@ <severity value="CRITICAL"/> <testCaseId value="MAGETWO-76081"/> <group value="configurable_product"/> - <skip> - <issueId value="MAGETWO-96271"/> - </skip> </annotations> <before> <createData entity="ApiCategory" stepKey="createCategory"/> diff --git a/app/code/Magento/ConfigurableProduct/Test/Unit/Block/Cart/Item/Renderer/ConfigurableTest.php b/app/code/Magento/ConfigurableProduct/Test/Unit/Block/Cart/Item/Renderer/ConfigurableTest.php index f324494f5918a..698a7ac9f2693 100644 --- a/app/code/Magento/ConfigurableProduct/Test/Unit/Block/Cart/Item/Renderer/ConfigurableTest.php +++ b/app/code/Magento/ConfigurableProduct/Test/Unit/Block/Cart/Item/Renderer/ConfigurableTest.php @@ -8,6 +8,9 @@ use Magento\Catalog\Model\Config\Source\Product\Thumbnail as ThumbnailSource; use Magento\ConfigurableProduct\Block\Cart\Item\Renderer\Configurable as Renderer; +/** + * @SuppressWarnings(PHPMD.CouplingBetweenObjects) + */ class ConfigurableTest extends \PHPUnit\Framework\TestCase { /** @@ -30,6 +33,16 @@ class ConfigurableTest extends \PHPUnit\Framework\TestCase */ private $productConfigMock; + /** + * @var \Magento\Backend\Block\Template\Context|\PHPUnit_Framework_MockObject_MockObject + */ + private $contextMock; + + /** + * @var \Magento\Framework\View\Layout|PHPUnit_Framework_MockObject_MockObject + */ + private $layoutMock; + /** * @var Renderer */ @@ -39,6 +52,10 @@ protected function setUp() { parent::setUp(); $objectManagerHelper = new \Magento\Framework\TestFramework\Unit\Helper\ObjectManager($this); + $this->contextMock = $this->createPartialMock(\Magento\Backend\Block\Template\Context::class, ['getLayout']); + $this->layoutMock = $this->createPartialMock(\Magento\Framework\View\Layout::class, ['getBlock']); + $this->contextMock->expects($this->once())->method('getLayout')->willReturn($this->layoutMock); + $this->configManager = $this->createMock(\Magento\Framework\View\ConfigInterface::class); $this->imageHelper = $this->createPartialMock( \Magento\Catalog\Helper\Image::class, @@ -53,6 +70,7 @@ protected function setUp() 'imageHelper' => $this->imageHelper, 'scopeConfig' => $this->scopeConfig, 'productConfig' => $this->productConfigMock, + 'context' => $this->contextMock, ] ); } @@ -75,4 +93,42 @@ public function testGetIdentities() $this->renderer->setItem($item); $this->assertEquals(array_merge($productTags, $productTags), $this->renderer->getIdentities()); } + + /** + * Product price renderer test. + * + * @return void + */ + public function testGetProductPriceHtml() + { + $priceHtml = 'some price html'; + $productMock = $this->createMock(\Magento\Catalog\Model\Product::class); + + $item = $this->createMock(\Magento\Quote\Model\Quote\Item::class); + $item->expects($this->atLeastOnce())->method('getProduct')->willReturn($productMock); + + $priceRenderMock = $this->getMockBuilder(\Magento\Framework\Pricing\Render::class) + ->disableOriginalConstructor() + ->getMock(); + + $this->layoutMock->expects($this->once()) + ->method('getBlock') + ->with('product.price.render.default') + ->willReturn($priceRenderMock); + + $priceRenderMock->expects($this->once()) + ->method('render') + ->with( + \Magento\Catalog\Pricing\Price\ConfiguredPriceInterface::CONFIGURED_PRICE_CODE, + $productMock, + [ + 'include_container' => true, + 'display_minimal_price' => true, + 'zone' => \Magento\Framework\Pricing\Render::ZONE_ITEM_LIST, + ] + )->willReturn($priceHtml); + + $this->renderer->setItem($item); + $this->assertEquals($priceHtml, $this->renderer->getProductPriceHtml($productMock)); + } } diff --git a/app/code/Magento/ConfigurableProduct/Test/Unit/Plugin/Catalog/Model/Product/Pricing/Renderer/SalableResolverTest.php b/app/code/Magento/ConfigurableProduct/Test/Unit/Plugin/Catalog/Model/Product/Pricing/Renderer/SalableResolverTest.php new file mode 100644 index 0000000000000..71bd6fd83c24d --- /dev/null +++ b/app/code/Magento/ConfigurableProduct/Test/Unit/Plugin/Catalog/Model/Product/Pricing/Renderer/SalableResolverTest.php @@ -0,0 +1,87 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\ConfigurableProduct\Test\Unit\Plugin\Catalog\Model\Product\Pricing\Renderer; + +use Magento\Catalog\Model\Product\Pricing\Renderer\SalableResolver; +use Magento\ConfigurableProduct\Model\Product\Type\Configurable as TypeConfigurable; +use Magento\ConfigurableProduct\Plugin\Catalog\Model\Product\Pricing\Renderer\SalableResolver as SalableResolverPlugin; +use Magento\Framework\Pricing\SaleableInterface; +use PHPUnit_Framework_MockObject_MockObject as MockObject; +use PHPUnit\Framework\TestCase; + +/** + * Class SalableResolverTest + */ +class SalableResolverTest extends TestCase +{ + /** + * @var TypeConfigurable|MockObject + */ + private $typeConfigurable; + + /** + * @var SalableResolverPlugin + */ + private $salableResolver; + + protected function setUp() + { + $this->typeConfigurable = $this->createMock(TypeConfigurable::class); + $this->salableResolver = new SalableResolverPlugin($this->typeConfigurable); + } + + /** + * @param SaleableInterface|MockObject $salableItem + * @param bool $isSalable + * @param bool $typeIsSalable + * @param bool $expectedResult + * @return void + * @dataProvider afterIsSalableDataProvider + */ + public function testAfterIsSalable($salableItem, bool $isSalable, bool $typeIsSalable, bool $expectedResult) + { + $salableResolver = $this->createMock(SalableResolver::class); + + $this->typeConfigurable->method('isSalable') + ->willReturn($typeIsSalable); + + $result = $this->salableResolver->afterIsSalable($salableResolver, $isSalable, $salableItem); + $this->assertEquals($expectedResult, $result); + } + + /** + * Data provider for testAfterIsSalable + * + * @return array + */ + public function afterIsSalableDataProvider(): array + { + $simpleSalableItem = $this->createMock(SaleableInterface::class); + $simpleSalableItem->method('getTypeId') + ->willReturn('simple'); + + $configurableSalableItem = $this->createMock(SaleableInterface::class); + $configurableSalableItem->method('getTypeId') + ->willReturn('configurable'); + + return [ + [ + $simpleSalableItem, + true, + false, + true, + ], + [ + $configurableSalableItem, + true, + false, + false, + ], + ]; + } +} diff --git a/app/code/Magento/ConfigurableProduct/view/adminhtml/web/js/components/price-configurable.js b/app/code/Magento/ConfigurableProduct/view/adminhtml/web/js/components/price-configurable.js index 28e775b984b05..6bbab77a3a0ab 100644 --- a/app/code/Magento/ConfigurableProduct/view/adminhtml/web/js/components/price-configurable.js +++ b/app/code/Magento/ConfigurableProduct/view/adminhtml/web/js/components/price-configurable.js @@ -53,6 +53,8 @@ define([ if (isConfigurable) { this.disable(); this.clear(); + } else { + this.enable(); } } }); diff --git a/app/code/Magento/ConfigurableProduct/view/adminhtml/web/js/variations/variations.js b/app/code/Magento/ConfigurableProduct/view/adminhtml/web/js/variations/variations.js index e2e0faec3b805..1d251f8ecc333 100644 --- a/app/code/Magento/ConfigurableProduct/view/adminhtml/web/js/variations/variations.js +++ b/app/code/Magento/ConfigurableProduct/view/adminhtml/web/js/variations/variations.js @@ -383,7 +383,11 @@ define([ * Chose action for the form save button */ saveFormHandler: function () { - this.serializeData(); + this.formElement().validate(); + + if (this.formElement().source.get('params.invalid') === false) { + this.serializeData(); + } if (this.checkForNewAttributes()) { this.formSaveParams = arguments; diff --git a/app/code/Magento/ConfigurableProduct/view/base/templates/product/price/tier_price.phtml b/app/code/Magento/ConfigurableProduct/view/base/templates/product/price/tier_price.phtml index 18f96cfaaf398..325ee1d5d79b3 100644 --- a/app/code/Magento/ConfigurableProduct/view/base/templates/product/price/tier_price.phtml +++ b/app/code/Magento/ConfigurableProduct/view/base/templates/product/price/tier_price.phtml @@ -15,10 +15,13 @@ + '</span>' + '</span>'; %> <li class="item"> - <%= $t('Buy %1 for %2 each and').replace('%1', item.qty).replace('%2', priceStr) %> - <strong class="benefit"> - <%= $t('save') %><span class="percent tier-<%= key %>"> <%= item.percentage %></span>% - </strong> + <%= '<?= $block->escapeHtml(__('Buy %1 for %2 each and', '%1', '%2')) ?>' + .replace('%1', item.qty) + .replace('%2', priceStr) %> + <strong class="benefit"> + <?= $block->escapeHtml(__('save')) ?><span + class="percent tier-<%= key %>"> <%= item.percentage %></span>% + </strong> </li> <% }); %> </ul> diff --git a/app/code/Magento/ConfigurableProduct/view/frontend/web/js/configurable.js b/app/code/Magento/ConfigurableProduct/view/frontend/web/js/configurable.js index 78974877dd90d..1df84d27a5c30 100644 --- a/app/code/Magento/ConfigurableProduct/view/frontend/web/js/configurable.js +++ b/app/code/Magento/ConfigurableProduct/view/frontend/web/js/configurable.js @@ -291,6 +291,8 @@ define([ images = this.options.spConfig.images[this.simpleProduct]; if (images) { + images = this._sortImages(images); + if (this.options.gallerySwitchStrategy === 'prepend') { images = images.concat(initialImages); } @@ -309,7 +311,17 @@ define([ $(this.options.mediaGallerySelector).AddFotoramaVideoEvents(); } - galleryObject.first(); + }, + + /** + * Sorting images array + * + * @private + */ + _sortImages: function (images) { + return _.sortBy(images, function (image) { + return image.position; + }); }, /** @@ -364,7 +376,8 @@ define([ basePrice = parseFloat(this.options.spConfig.prices.basePrice.amount), optionFinalPrice, optionPriceDiff, - optionPrices = this.options.spConfig.optionPrices; + optionPrices = this.options.spConfig.optionPrices, + allowedProductMinPrice; this._clearSelect(element); element.options[0] = new Option('', ''); @@ -395,8 +408,8 @@ define([ if (typeof allowedProducts[0] !== 'undefined' && typeof optionPrices[allowedProducts[0]] !== 'undefined') { - - optionFinalPrice = parseFloat(optionPrices[allowedProducts[0]].finalPrice.amount); + allowedProductMinPrice = this._getAllowedProductWithMinPrice(allowedProducts); + optionFinalPrice = parseFloat(optionPrices[allowedProductMinPrice].finalPrice.amount); optionPriceDiff = optionFinalPrice - basePrice; if (optionPriceDiff !== 0) { @@ -477,36 +490,27 @@ define([ _getPrices: function () { var prices = {}, elements = _.toArray(this.options.settings), - hasProductPrice = false, - optionPriceDiff = 0, - allowedProduct, optionPrices, basePrice, optionFinalPrice; + allowedProduct; _.each(elements, function (element) { var selected = element.options[element.selectedIndex], config = selected && selected.config, priceValue = {}; - if (config && config.allowedProducts.length === 1 && !hasProductPrice) { - prices = {}; + if (config && config.allowedProducts.length === 1) { priceValue = this._calculatePrice(config); - hasProductPrice = true; } else if (element.value) { allowedProduct = this._getAllowedProductWithMinPrice(config.allowedProducts); - optionPrices = this.options.spConfig.optionPrices; - basePrice = parseFloat(this.options.spConfig.prices.basePrice.amount); - - if (!_.isEmpty(allowedProduct)) { - optionFinalPrice = parseFloat(optionPrices[allowedProduct].finalPrice.amount); - optionPriceDiff = optionFinalPrice - basePrice; - } - - if (optionPriceDiff !== 0) { - prices = {}; - priceValue = this._calculatePriceDifference(allowedProduct); - } + priceValue = this._calculatePrice({ + 'allowedProducts': [ + allowedProduct + ] + }); } - prices[element.attributeId] = priceValue; + if (!_.isEmpty(priceValue)) { + prices.prices = priceValue; + } }, this); return prices; @@ -527,40 +531,15 @@ define([ _.each(allowedProducts, function (allowedProduct) { optionFinalPrice = parseFloat(optionPrices[allowedProduct].finalPrice.amount); - if (_.isEmpty(product)) { + if (_.isEmpty(product) || optionFinalPrice < optionMinPrice) { optionMinPrice = optionFinalPrice; product = allowedProduct; } - - if (optionFinalPrice < optionMinPrice) { - product = allowedProduct; - } }, this); return product; }, - /** - * Calculate price difference for allowed product - * - * @param {*} allowedProduct - Product - * @returns {*} - * @private - */ - _calculatePriceDifference: function (allowedProduct) { - var displayPrices = $(this.options.priceHolderSelector).priceBox('option').prices, - newPrices = this.options.spConfig.optionPrices[allowedProduct]; - - _.each(displayPrices, function (price, code) { - - if (newPrices[code]) { - displayPrices[code].amount = newPrices[code].amount - displayPrices[code].amount; - } - }); - - return displayPrices; - }, - /** * Returns prices for configured products * diff --git a/app/code/Magento/Contact/view/frontend/email/submitted_form.html b/app/code/Magento/Contact/view/frontend/email/submitted_form.html index 1bce6159c586a..17146257aeff1 100644 --- a/app/code/Magento/Contact/view/frontend/email/submitted_form.html +++ b/app/code/Magento/Contact/view/frontend/email/submitted_form.html @@ -16,19 +16,19 @@ <table class="message-details"> <tr> - <td><b>{{trans "Name"}}</b></td> + <td><strong>{{trans "Name"}}</strong></td> <td>{{var data.name}}</td> </tr> <tr> - <td><b>{{trans "Email"}}</b></td> + <td><strong>{{trans "Email"}}</strong></td> <td>{{var data.email}}</td> </tr> <tr> - <td><b>{{trans "Phone"}}</b></td> + <td><strong>{{trans "Phone"}}</strong></td> <td>{{var data.telephone}}</td> </tr> </table> -<p><b>{{trans "Message"}}</b></p> +<p><strong>{{trans "Message"}}</strong></p> <p>{{var data.comment}}</p> {{template config_path="design/email/footer_template"}} diff --git a/app/code/Magento/Contact/view/frontend/web/css/source/_module.less b/app/code/Magento/Contact/view/frontend/web/css/source/_module.less new file mode 100644 index 0000000000000..0aaec05aa2afe --- /dev/null +++ b/app/code/Magento/Contact/view/frontend/web/css/source/_module.less @@ -0,0 +1,42 @@ +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. +*/ + +& when (@media-common = true) { + .contact-index-index { + .column:not(.sidebar-main) { + .form.contact { + float: none; + width: 50%; + } + } + + .column:not(.sidebar-additional) { + .form.contact { + float: none; + width: 50%; + } + } + } +} + +// Mobile +.media-width(@extremum, @break) when (@extremum = 'max') and (@break = @screen__m) { + .contact-index-index { + .column:not(.sidebar-main) { + .form.contact { + float: none; + width: 100%; + } + } + + .column:not(.sidebar-additional) { + .form.contact { + float: none; + width: 100%; + } + } + } +} + diff --git a/app/code/Magento/CurrencySymbol/view/adminhtml/templates/system/currency/rate/matrix.phtml b/app/code/Magento/CurrencySymbol/view/adminhtml/templates/system/currency/rate/matrix.phtml index 8e0abcb319764..8a16eb71e0853 100644 --- a/app/code/Magento/CurrencySymbol/view/adminhtml/templates/system/currency/rate/matrix.phtml +++ b/app/code/Magento/CurrencySymbol/view/adminhtml/templates/system/currency/rate/matrix.phtml @@ -45,7 +45,7 @@ $_rates = ($_newRates) ? $_newRates : $_oldRates; class="admin__control-text" <?= ($_currencyCode == $_rate) ? ' disabled' : '' ?> /> <?php if (isset($_newRates) && $_currencyCode != $_rate && isset($_oldRates[$_currencyCode][$_rate])): ?> - <div class="admin__field-note"><?= /* @escapeNotVerified */ __('Old rate:') ?> <b><?= /* @escapeNotVerified */ $_oldRates[$_currencyCode][$_rate] ?></b></div> + <div class="admin__field-note"><?= /* @escapeNotVerified */ __('Old rate:') ?> <strong><?= /* @escapeNotVerified */ $_oldRates[$_currencyCode][$_rate] ?></strong></div> <?php endif; ?> </td> <?php else: ?> @@ -56,7 +56,7 @@ $_rates = ($_newRates) ? $_newRates : $_oldRates; class="admin__control-text" <?= ($_currencyCode == $_rate) ? ' disabled' : '' ?> /> <?php if (isset($_newRates) && $_currencyCode != $_rate && isset($_oldRates[$_currencyCode][$_rate])): ?> - <div class="admin__field-note"><?= /* @escapeNotVerified */ __('Old rate:') ?> <b><?= /* @escapeNotVerified */ $_oldRates[$_currencyCode][$_rate] ?></b></div> + <div class="admin__field-note"><?= /* @escapeNotVerified */ __('Old rate:') ?> <strong><?= /* @escapeNotVerified */ $_oldRates[$_currencyCode][$_rate] ?></strong></div> <?php endif; ?> </td> <?php endif; ?> diff --git a/app/code/Magento/Customer/Block/DataProviders/PostCodesPatternsAttributeData.php b/app/code/Magento/Customer/Block/DataProviders/PostCodesPatternsAttributeData.php new file mode 100644 index 0000000000000..280948439e1f8 --- /dev/null +++ b/app/code/Magento/Customer/Block/DataProviders/PostCodesPatternsAttributeData.php @@ -0,0 +1,50 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Customer\Block\DataProviders; + +use Magento\Framework\Serialize\SerializerInterface; +use Magento\Framework\View\Element\Block\ArgumentInterface; +use Magento\Directory\Model\Country\Postcode\Config as PostCodeConfig; + +/** + * Provides postcodes patterns into template. + */ +class PostCodesPatternsAttributeData implements ArgumentInterface +{ + /** + * @var PostCodeConfig + */ + private $postCodeConfig; + + /** + * @var SerializerInterface + */ + private $serializer; + + /** + * Constructor + * + * @param PostCodeConfig $postCodeConfig + * @param SerializerInterface $serializer + */ + public function __construct(PostCodeConfig $postCodeConfig, SerializerInterface $serializer) + { + $this->postCodeConfig = $postCodeConfig; + $this->serializer = $serializer; + } + + /** + * Get serialized post codes + * + * @return string + */ + public function getSerializedPostCodes(): string + { + return $this->serializer->serialize($this->postCodeConfig->getPostCodes()); + } +} diff --git a/app/code/Magento/Customer/Model/AccountManagement.php b/app/code/Magento/Customer/Model/AccountManagement.php index b5b1905e68d8d..6387555385e99 100644 --- a/app/code/Magento/Customer/Model/AccountManagement.php +++ b/app/code/Magento/Customer/Model/AccountManagement.php @@ -17,6 +17,7 @@ use Magento\Customer\Model\Customer as CustomerModel; use Magento\Customer\Model\Customer\CredentialsValidator; use Magento\Customer\Model\Metadata\Validator; +use Magento\Customer\Model\ResourceModel\Visitor\CollectionFactory; use Magento\Eav\Model\Validator\Attribute\Backend; use Magento\Framework\Api\ExtensibleDataObjectConverter; use Magento\Framework\Api\SearchCriteriaBuilder; @@ -44,14 +45,13 @@ use Magento\Framework\Phrase; use Magento\Framework\Reflection\DataObjectProcessor; use Magento\Framework\Registry; +use Magento\Framework\Session\SaveHandlerInterface; +use Magento\Framework\Session\SessionManagerInterface; use Magento\Framework\Stdlib\DateTime; use Magento\Framework\Stdlib\StringUtils as StringHelper; use Magento\Store\Model\ScopeInterface; use Magento\Store\Model\StoreManagerInterface; use Psr\Log\LoggerInterface as PsrLogger; -use Magento\Framework\Session\SessionManagerInterface; -use Magento\Framework\Session\SaveHandlerInterface; -use Magento\Customer\Model\ResourceModel\Visitor\CollectionFactory; /** * Handle various customer account actions @@ -332,6 +332,11 @@ class AccountManagement implements AccountManagementInterface */ private $searchCriteriaBuilder; + /** + * @var AddressRegistry + */ + private $addressRegistry; + /** * @param CustomerFactory $customerFactory * @param ManagerInterface $eventManager @@ -359,12 +364,13 @@ class AccountManagement implements AccountManagementInterface * @param CredentialsValidator|null $credentialsValidator * @param DateTimeFactory|null $dateTimeFactory * @param AccountConfirmation|null $accountConfirmation - * @param DateTimeFactory $dateTimeFactory * @param SessionManagerInterface|null $sessionManager * @param SaveHandlerInterface|null $saveHandler * @param CollectionFactory|null $visitorCollectionFactory * @param SearchCriteriaBuilder|null $searchCriteriaBuilder + * @param AddressRegistry|null $addressRegistry * @SuppressWarnings(PHPMD.ExcessiveParameterList) + * @SuppressWarnings(PHPMD.NPathComplexity) */ public function __construct( CustomerFactory $customerFactory, @@ -396,7 +402,8 @@ public function __construct( SessionManagerInterface $sessionManager = null, SaveHandlerInterface $saveHandler = null, CollectionFactory $visitorCollectionFactory = null, - SearchCriteriaBuilder $searchCriteriaBuilder = null + SearchCriteriaBuilder $searchCriteriaBuilder = null, + AddressRegistry $addressRegistry = null ) { $this->customerFactory = $customerFactory; $this->eventManager = $eventManager; @@ -434,6 +441,8 @@ public function __construct( ?: ObjectManager::getInstance()->get(CollectionFactory::class); $this->searchCriteriaBuilder = $searchCriteriaBuilder ?: ObjectManager::getInstance()->get(SearchCriteriaBuilder::class); + $this->addressRegistry = $addressRegistry + ?: ObjectManager::getInstance()->get(AddressRegistry::class); } /** @@ -499,8 +508,11 @@ public function activateById($customerId, $confirmationKey) * @param \Magento\Customer\Api\Data\CustomerInterface $customer * @param string $confirmationKey * @return \Magento\Customer\Api\Data\CustomerInterface - * @throws \Magento\Framework\Exception\State\InvalidTransitionException - * @throws \Magento\Framework\Exception\State\InputMismatchException + * @throws InputException + * @throws InputMismatchException + * @throws InvalidTransitionException + * @throws LocalizedException + * @throws NoSuchEntityException */ private function activateCustomer($customer, $confirmationKey) { @@ -574,6 +586,9 @@ public function initiatePasswordReset($email, $template, $websiteId = null) // load customer by email $customer = $this->customerRepository->get($email, $websiteId); + // No need to validate customer address while saving customer reset password token + $this->disableAddressValidation($customer); + $newPasswordToken = $this->mathRandom->getUniqueHash(); $this->changeResetPasswordLinkToken($customer, $newPasswordToken); @@ -611,10 +626,10 @@ public function initiatePasswordReset($email, $template, $websiteId = null) * Match a customer by their RP token. * * @param string $rpToken + * @return CustomerInterface * @throws ExpiredException + * @throws LocalizedException * @throws NoSuchEntityException - * - * @return CustomerInterface */ private function matchCustomerByRpToken(string $rpToken): CustomerInterface { @@ -657,6 +672,10 @@ public function resetPassword($email, $resetToken, $newPassword) } else { $customer = $this->customerRepository->get($email); } + + // No need to validate customer address while saving customer reset password token + $this->disableAddressValidation($customer); + //Validate Token and new password strength $this->validateResetPasswordToken($customer->getId(), $resetToken); $this->credentialsValidator->checkPasswordDifferentFromEmail( @@ -781,7 +800,7 @@ public function getConfirmationStatus($customerId) /** * {@inheritdoc} */ - public function createAccount(CustomerInterface $customer, $password = null, $redirectUrl = '') + public function createAccount(CustomerInterface $customer, $password = null, $redirectUrl = '', $extensions = []) { if ($password !== null) { $this->checkPasswordStrength($password); @@ -795,7 +814,8 @@ public function createAccount(CustomerInterface $customer, $password = null, $re } else { $hash = null; } - return $this->createAccountWithPasswordHash($customer, $hash, $redirectUrl); + + return $this->createAccountWithPasswordHash($customer, $hash, $redirectUrl, $extensions); } /** @@ -803,8 +823,12 @@ public function createAccount(CustomerInterface $customer, $password = null, $re * @SuppressWarnings(PHPMD.CyclomaticComplexity) * @SuppressWarnings(PHPMD.NPathComplexity) */ - public function createAccountWithPasswordHash(CustomerInterface $customer, $hash, $redirectUrl = '') - { + public function createAccountWithPasswordHash( + CustomerInterface $customer, + $hash, + $redirectUrl = '', + $extensions = [] + ) { // This logic allows an existing customer to be added to a different store. No new account is created. // The plan is to move this logic into a new method called something like 'registerAccountWithStore' if ($customer->getId()) { @@ -871,7 +895,7 @@ public function createAccountWithPasswordHash(CustomerInterface $customer, $hash $customer = $this->customerRepository->getById($customer->getId()); $newLinkToken = $this->mathRandom->getUniqueHash(); $this->changeResetPasswordLinkToken($customer, $newLinkToken); - $this->sendEmailConfirmation($customer, $redirectUrl); + $this->sendEmailConfirmation($customer, $redirectUrl, $extensions); return $customer; } @@ -899,9 +923,12 @@ public function getDefaultShippingAddress($customerId) * * @param CustomerInterface $customer * @param string $redirectUrl + * @param array $extensions * @return void + * @throws LocalizedException + * @throws NoSuchEntityException */ - protected function sendEmailConfirmation(CustomerInterface $customer, $redirectUrl) + protected function sendEmailConfirmation(CustomerInterface $customer, $redirectUrl, $extensions = []) { try { $hash = $this->customerRegistry->retrieveSecureData($customer->getId())->getPasswordHash(); @@ -911,7 +938,14 @@ protected function sendEmailConfirmation(CustomerInterface $customer, $redirectU } elseif ($hash == '') { $templateType = self::NEW_ACCOUNT_EMAIL_REGISTERED_NO_PASSWORD; } - $this->getEmailNotification()->newAccount($customer, $templateType, $redirectUrl, $customer->getStoreId()); + $this->getEmailNotification()->newAccount( + $customer, + $templateType, + $redirectUrl, + $customer->getStoreId(), + null, + $extensions + ); } catch (MailException $e) { // If we are not able to send a new account email, this should be ignored $this->logger->critical($e); @@ -954,7 +988,10 @@ public function changePasswordById($customerId, $currentPassword, $newPassword) * @param string $newPassword * @return bool true on success * @throws InputException + * @throws InputMismatchException * @throws InvalidEmailOrPasswordException + * @throws LocalizedException + * @throws NoSuchEntityException * @throws UserLockedException */ private function changePasswordForCustomer($customer, $currentPassword, $newPassword) @@ -1063,10 +1100,11 @@ public function isCustomerInStore($customerWebsiteId, $storeId) * @param int $customerId * @param string $resetPasswordLinkToken * @return bool - * @throws \Magento\Framework\Exception\State\InputMismatchException If token is mismatched - * @throws \Magento\Framework\Exception\State\ExpiredException If token is expired - * @throws \Magento\Framework\Exception\InputException If token or customer id is invalid - * @throws \Magento\Framework\Exception\NoSuchEntityException If customer doesn't exist + * @throws ExpiredException + * @throws InputException + * @throws InputMismatchException + * @throws LocalizedException + * @throws NoSuchEntityException */ private function validateResetPasswordToken($customerId, $resetPasswordLinkToken) { @@ -1156,6 +1194,8 @@ protected function sendNewAccountEmail( * * @param CustomerInterface $customer * @return $this + * @throws LocalizedException + * @throws NoSuchEntityException * @deprecated 100.1.0 */ protected function sendPasswordResetNotificationEmail($customer) @@ -1169,6 +1209,7 @@ protected function sendPasswordResetNotificationEmail($customer) * @param CustomerInterface $customer * @param int|string|null $defaultStoreId * @return int + * @throws LocalizedException * @deprecated 100.1.0 */ protected function getWebsiteStoreId($customer, $defaultStoreId = null) @@ -1182,6 +1223,8 @@ protected function getWebsiteStoreId($customer, $defaultStoreId = null) } /** + * Get email template types + * * @return array * @deprecated 100.1.0 */ @@ -1215,6 +1258,7 @@ protected function getTemplateTypes() * @param int|null $storeId * @param string $email * @return $this + * @throws MailException * @deprecated 100.1.0 */ protected function sendEmailTemplate( @@ -1321,6 +1365,9 @@ public function isResetPasswordLinkTokenExpired($rpToken, $rpTokenCreatedAt) * @param string $passwordLinkToken * @return bool * @throws InputException + * @throws InputMismatchException + * @throws LocalizedException + * @throws NoSuchEntityException */ public function changeResetPasswordLinkToken($customer, $passwordLinkToken) { @@ -1348,6 +1395,8 @@ public function changeResetPasswordLinkToken($customer, $passwordLinkToken) * * @param CustomerInterface $customer * @return $this + * @throws LocalizedException + * @throws NoSuchEntityException * @deprecated 100.1.0 */ public function sendPasswordReminderEmail($customer) @@ -1375,6 +1424,8 @@ public function sendPasswordReminderEmail($customer) * * @param CustomerInterface $customer * @return $this + * @throws LocalizedException + * @throws NoSuchEntityException * @deprecated 100.1.0 */ public function sendPasswordResetConfirmationEmail($customer) @@ -1419,6 +1470,7 @@ protected function getAddressById(CustomerInterface $customer, $addressId) * * @param CustomerInterface $customer * @return Data\CustomerSecure + * @throws NoSuchEntityException * @deprecated 100.1.0 */ protected function getFullCustomerObject($customer) @@ -1444,6 +1496,20 @@ public function getPasswordHash($password) return $this->encryptor->getHash($password); } + /** + * Disable Customer Address Validation + * + * @param CustomerInterface $customer + * @throws NoSuchEntityException + */ + private function disableAddressValidation($customer) + { + foreach ($customer->getAddresses() as $address) { + $addressModel = $this->addressRegistry->retrieve($address->getId()); + $addressModel->setShouldIgnoreValidation(true); + } + } + /** * Get email notification * diff --git a/app/code/Magento/Customer/Model/EmailNotification.php b/app/code/Magento/Customer/Model/EmailNotification.php index 740dc33b72710..679c108ab1d30 100644 --- a/app/code/Magento/Customer/Model/EmailNotification.php +++ b/app/code/Magento/Customer/Model/EmailNotification.php @@ -17,6 +17,8 @@ use Magento\Framework\Exception\LocalizedException; /** + * Class for notification customer. + * * @SuppressWarnings(PHPMD.CouplingBetweenObjects) */ class EmailNotification implements EmailNotificationInterface @@ -63,6 +65,8 @@ class EmailNotification implements EmailNotificationInterface self::NEW_ACCOUNT_EMAIL_CONFIRMATION => self::XML_PATH_CONFIRM_EMAIL_TEMPLATE, ]; + const CUSTOMER_CONFIRM_URL = 'customer/account/confirm/'; + /**#@-*/ /**#@-*/ @@ -362,6 +366,7 @@ public function passwordResetConfirmation(CustomerInterface $customer) * @param string $backUrl * @param string $storeId * @param string $sendemailStoreId + * @param array $extensions * @return void * @throws LocalizedException */ @@ -370,7 +375,8 @@ public function newAccount( $type = self::NEW_ACCOUNT_EMAIL_REGISTERED, $backUrl = '', $storeId = 0, - $sendemailStoreId = null + $sendemailStoreId = null, + $extensions = [] ) { $types = self::TEMPLATE_TYPES; @@ -386,11 +392,26 @@ public function newAccount( $customerEmailData = $this->getFullCustomerObject($customer); + $templateVars = [ + 'customer' => $customerEmailData, + 'back_url' => $backUrl, + 'store' => $store, + ]; + if ($type == self::NEW_ACCOUNT_EMAIL_CONFIRMATION) { + if (empty($extensions)) { + $templateVars['url'] = self::CUSTOMER_CONFIRM_URL; + $templateVars['extensions'] = $extensions; + } else { + $templateVars['url'] = $extensions['url']; + $templateVars['extensions'] = $extensions['extension_info']; + } + } + $this->sendEmailTemplate( $customer, $types[$type], self::XML_PATH_REGISTER_EMAIL_IDENTITY, - ['customer' => $customerEmailData, 'back_url' => $backUrl, 'store' => $store], + $templateVars, $storeId ); } diff --git a/app/code/Magento/Customer/Model/Metadata/CustomerMetadata.php b/app/code/Magento/Customer/Model/Metadata/CustomerMetadata.php index 7ed806e657e82..a9753fc810a99 100644 --- a/app/code/Magento/Customer/Model/Metadata/CustomerMetadata.php +++ b/app/code/Magento/Customer/Model/Metadata/CustomerMetadata.php @@ -33,16 +33,26 @@ class CustomerMetadata implements CustomerMetadataInterface */ private $attributeMetadataDataProvider; + /** + * List of system attributes which should be available to the clients. + * + * @var string[] + */ + private $systemAttributes; + /** * @param AttributeMetadataConverter $attributeMetadataConverter * @param AttributeMetadataDataProvider $attributeMetadataDataProvider + * @param string[] $systemAttributes */ public function __construct( AttributeMetadataConverter $attributeMetadataConverter, - AttributeMetadataDataProvider $attributeMetadataDataProvider + AttributeMetadataDataProvider $attributeMetadataDataProvider, + array $systemAttributes = [] ) { $this->attributeMetadataConverter = $attributeMetadataConverter; $this->attributeMetadataDataProvider = $attributeMetadataDataProvider; + $this->systemAttributes = $systemAttributes; } /** @@ -116,7 +126,7 @@ public function getAllAttributesMetadata() } /** - * {@inheritdoc} + * @inheritdoc */ public function getCustomAttributesMetadata($dataObjectClassName = self::DATA_INTERFACE_NAME) { @@ -134,9 +144,10 @@ public function getCustomAttributesMetadata($dataObjectClassName = self::DATA_IN $isDataObjectMethod = isset($this->customerDataObjectMethods['get' . $camelCaseKey]) || isset($this->customerDataObjectMethods['is' . $camelCaseKey]); - /** Even though disable_auto_group_change is system attribute, it should be available to the clients */ if (!$isDataObjectMethod - && (!$attributeMetadata->isSystem() || $attributeCode == 'disable_auto_group_change') + && (!$attributeMetadata->isSystem() + || in_array($attributeCode, $this->systemAttributes) + ) ) { $customAttributes[] = $attributeMetadata; } diff --git a/app/code/Magento/Customer/Model/Metadata/Form/AbstractData.php b/app/code/Magento/Customer/Model/Metadata/Form/AbstractData.php index f28cce0ea2ae1..0e4fc68503122 100644 --- a/app/code/Magento/Customer/Model/Metadata/Form/AbstractData.php +++ b/app/code/Magento/Customer/Model/Metadata/Form/AbstractData.php @@ -1,7 +1,5 @@ <?php /** - * Form Element Abstract Data Model - * * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ @@ -14,6 +12,8 @@ use Magento\Framework\Validator\EmailAddress; /** + * Form Element Abstract Data Model. + * * @SuppressWarnings(PHPMD.CouplingBetweenObjects) */ abstract class AbstractData @@ -138,7 +138,8 @@ public function setRequestScope($scope) } /** - * Set scope visibility + * Set scope visibility. + * * Search value only in scope or search value in scope and global * * @param boolean $flag @@ -283,9 +284,13 @@ protected function _validateInputRule($value) ); if (!is_null($inputValidation)) { + $allowWhiteSpace = false; switch ($inputValidation) { + case 'alphanum-with-spaces': + $allowWhiteSpace = true; + // continue to alphanumeric validation case 'alphanumeric': - $validator = new \Zend_Validate_Alnum(true); + $validator = new \Zend_Validate_Alnum($allowWhiteSpace); $validator->setMessage(__('"%1" invalid type entered.', $label), \Zend_Validate_Alnum::INVALID); $validator->setMessage( __('"%1" contains non-alphabetic or non-numeric characters.', $label), diff --git a/app/code/Magento/Customer/Model/ResourceModel/CustomerRepository.php b/app/code/Magento/Customer/Model/ResourceModel/CustomerRepository.php index 9e745769e2c36..d96182202eb7d 100644 --- a/app/code/Magento/Customer/Model/ResourceModel/CustomerRepository.php +++ b/app/code/Magento/Customer/Model/ResourceModel/CustomerRepository.php @@ -365,7 +365,7 @@ public function getList(SearchCriteriaInterface $searchCriteria) ->joinAttribute('billing_telephone', 'customer_address/telephone', 'default_billing', null, 'left') ->joinAttribute('billing_region', 'customer_address/region', 'default_billing', null, 'left') ->joinAttribute('billing_country_id', 'customer_address/country_id', 'default_billing', null, 'left') - ->joinAttribute('company', 'customer_address/company', 'default_billing', null, 'left'); + ->joinAttribute('billing_company', 'customer_address/company', 'default_billing', null, 'left'); $this->collectionProcessor->process($searchCriteria, $collection); diff --git a/app/code/Magento/Customer/Test/Mftf/ActionGroup/CustomerActionGroup.xml b/app/code/Magento/Customer/Test/Mftf/ActionGroup/CustomerActionGroup.xml index 6a253a6053076..959af9f70db29 100644 --- a/app/code/Magento/Customer/Test/Mftf/ActionGroup/CustomerActionGroup.xml +++ b/app/code/Magento/Customer/Test/Mftf/ActionGroup/CustomerActionGroup.xml @@ -13,7 +13,6 @@ <argument name="customer" defaultValue="customer"/> </arguments> <amOnPage stepKey="loginPage" url="customer/account/login/"/> - <waitForPageLoad stepKey="pageLoadBeforeLogin"/> <fillField stepKey="fillEmail" userInput="{{customer.email}}" selector="{{StorefrontCustomerSignInFormSection.emailField}}"/> <fillField stepKey="fillPassword" userInput="{{customer.password}}" selector="{{StorefrontCustomerSignInFormSection.passwordField}}"/> <click stepKey="clickSignInAccountButton" selector="{{StorefrontCustomerSignInFormSection.signInAccountButton}}"/> @@ -21,6 +20,5 @@ <actionGroup name="CustomerLogoutStorefrontActionGroup"> <amOnPage url="customer/account/logout/" stepKey="storefrontSignOut"/> - <waitForPageLoad time="30" stepKey="waitForLogOut"/> </actionGroup> </actionGroups> diff --git a/app/code/Magento/Customer/Test/Mftf/ActionGroup/OpenEditCustomerFromAdminActionGroup.xml b/app/code/Magento/Customer/Test/Mftf/ActionGroup/OpenEditCustomerFromAdminActionGroup.xml index 3d6e0fb54b054..79b876ff6ecf7 100644 --- a/app/code/Magento/Customer/Test/Mftf/ActionGroup/OpenEditCustomerFromAdminActionGroup.xml +++ b/app/code/Magento/Customer/Test/Mftf/ActionGroup/OpenEditCustomerFromAdminActionGroup.xml @@ -6,13 +6,12 @@ */ --> <actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" - xsi:noNamespaceSchemaLocation="../../../../../../../dev/tests/acceptance/vendor/magento/magento2-functional-testing-framework/src/Magento/FunctionalTestingFramework/Test/etc/actionGroupSchema.xsd"> - <actionGroup name="OpenEditCustomerFromAdminActionGroup"> + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> + <actionGroup name="OpenEditCustomerFromAdminActionGroup" extends="clearFiltersAdminDataGrid"> <arguments> <argument name="customer"/> </arguments> - <amOnPage url="{{AdminCustomerPage.url}}" stepKey="navigateToCustomers"/> - <waitForPageLoad stepKey="waitForPageLoad1" /> + <amOnPage url="{{AdminCustomerPage.url}}" before="waitForPageLoad" stepKey="navigateToCustomers"/> <click selector="{{AdminCustomerFiltersSection.filtersButton}}" stepKey="openFilter"/> <fillField userInput="{{customer.email}}" selector="{{AdminCustomerFiltersSection.emailInput}}" stepKey="filterEmail"/> <click selector="{{AdminCustomerFiltersSection.apply}}" stepKey="applyFilter"/> diff --git a/app/code/Magento/Customer/Test/Mftf/ActionGroup/StorefrontCustomerAccountActionGroup.xml b/app/code/Magento/Customer/Test/Mftf/ActionGroup/StorefrontCustomerAccountActionGroup.xml new file mode 100644 index 0000000000000..50a238323e331 --- /dev/null +++ b/app/code/Magento/Customer/Test/Mftf/ActionGroup/StorefrontCustomerAccountActionGroup.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="StorefrontCustomerAccountCheckTab"> + <arguments> + <argument name="tabName" type="string"/> + </arguments> + + <see selector="{{StorefrontCustomerSidebarSection.sidebarTab(tabName)}}" userInput="{{tabName}}" stepKey="checkTabExists"/> + <click selector="{{StorefrontCustomerSidebarSection.sidebarTab(tabName)}}" stepKey="clickToOpenTab"/> + <waitForPageLoad stepKey="waitForPageLoad"/> + <see selector="{{StorefrontHeaderSection.mainTitle}}" userInput="{{tabName}}" stepKey="checkTabTitle"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/Customer/Test/Mftf/ActionGroup/StorefrontCustomerActionGroup.xml b/app/code/Magento/Customer/Test/Mftf/ActionGroup/StorefrontCustomerActionGroup.xml new file mode 100644 index 0000000000000..fc5c1b881752e --- /dev/null +++ b/app/code/Magento/Customer/Test/Mftf/ActionGroup/StorefrontCustomerActionGroup.xml @@ -0,0 +1,18 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> + <actionGroup name="CustomerLogoutStorefrontByMenuItemsActionGroup"> + <conditionalClick selector="{{StorefrontPanelHeaderSection.customerWelcome}}" + dependentSelector="{{StorefrontPanelHeaderSection.customerWelcomeMenu}}" + visible="false" + stepKey="clickHeaderCustomerMenuButton" /> + <click selector="{{StorefrontPanelHeaderSection.customerLogoutLink}}" stepKey="clickSignOutButton" /> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/Customer/Test/Mftf/Data/AddressData.xml b/app/code/Magento/Customer/Test/Mftf/Data/AddressData.xml index 8ffc9306ea37e..573767a12361a 100644 --- a/app/code/Magento/Customer/Test/Mftf/Data/AddressData.xml +++ b/app/code/Magento/Customer/Test/Mftf/Data/AddressData.xml @@ -67,6 +67,7 @@ <data key="state">California</data> <data key="postcode">90230</data> <data key="country_id">US</data> + <data key="country">United States</data> <data key="default_billing">Yes</data> <data key="default_shipping">Yes</data> <requiredEntity type="region">RegionCA</requiredEntity> @@ -85,10 +86,10 @@ <data key="state">New York</data> <data key="postcode">11001</data> <data key="country_id">US</data> + <data key="country">United States</data> <data key="default_billing">Yes</data> <data key="default_shipping">Yes</data> <requiredEntity type="region">RegionNY</requiredEntity> - <data key="country">United States</data> </entity> <entity name="UK_Default_Address" type="address"> <data key="firstname">Jane</data> @@ -101,6 +102,15 @@ <data key="state"></data> <data key="postcode">SE1 7RW</data> <data key="country_id">GB</data> + <data key="country">United Kingdom</data> <data key="telephone">444-44-444-44</data> </entity> + <entity name="US_Default_Billing_Address_TX" type="address" extends="US_Address_TX"> + <data key="default_billing">false</data> + <data key="default_shipping">true</data> + </entity> + <entity name="US_Default_Shipping_Address_CA" type="address" extends="US_Address_CA"> + <data key="default_billing">true</data> + <data key="default_shipping">false</data> + </entity> </entities> diff --git a/app/code/Magento/Customer/Test/Mftf/Data/CustomerConfigData.xml b/app/code/Magento/Customer/Test/Mftf/Data/CustomerConfigData.xml new file mode 100644 index 0000000000000..0ec9f79ecee52 --- /dev/null +++ b/app/code/Magento/Customer/Test/Mftf/Data/CustomerConfigData.xml @@ -0,0 +1,24 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<entities xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:DataGenerator/etc/dataProfileSchema.xsd"> + <entity name="CustomerAccountSharingDefault" type="customer_account_sharing_config"> + <requiredEntity type="account_share_scope_value">CustomerAccountSharingPerWebsite</requiredEntity> + </entity> + <entity name="CustomerAccountSharingPerWebsite" type="account_share_scope_value"> + <data key="value">1</data> + </entity> + + <entity name="CustomerAccountSharingGlobal" type="customer_account_sharing_config"> + <requiredEntity type="account_share_scope_value">GlobalCustomerAccountSharing</requiredEntity> + </entity> + <entity name="GlobalCustomerAccountSharing" type="account_share_scope_value"> + <data key="value">0</data> + </entity> +</entities> \ No newline at end of file diff --git a/app/code/Magento/Customer/Test/Mftf/Data/CustomerData.xml b/app/code/Magento/Customer/Test/Mftf/Data/CustomerData.xml index 74da84c01b861..a8b8cece39fad 100644 --- a/app/code/Magento/Customer/Test/Mftf/Data/CustomerData.xml +++ b/app/code/Magento/Customer/Test/Mftf/Data/CustomerData.xml @@ -33,7 +33,7 @@ <!--requiredEntity type="extension_attribute">ExtensionAttributeSimple</requiredEntity--> </entity> <entity name="Simple_US_Customer" type="customer"> - <data key="group_id">0</data> + <data key="group_id">1</data> <data key="default_billing">true</data> <data key="default_shipping">true</data> <data key="email" unique="prefix">John.Doe@example.com</data> @@ -74,4 +74,16 @@ <data key="website_id">0</data> <requiredEntity type="address">US_Address_NY</requiredEntity> </entity> + <entity name="Customer_With_Different_Default_Billing_Shipping_Addresses" type="customer"> + <data key="group_id">1</data> + <data key="email" unique="prefix">John.Doe@example.com</data> + <data key="firstname">John</data> + <data key="lastname">Doe</data> + <data key="fullname">John Doe</data> + <data key="password">pwdTest123!</data> + <data key="store_id">0</data> + <data key="website_id">0</data> + <requiredEntity type="address">US_Default_Billing_Address_TX</requiredEntity> + <requiredEntity type="address">US_Default_Shipping_Address_CA</requiredEntity> + </entity> </entities> diff --git a/app/code/Magento/Customer/Test/Mftf/Metadata/customer_config_account_sharing-meta.xml b/app/code/Magento/Customer/Test/Mftf/Metadata/customer_config_account_sharing-meta.xml new file mode 100644 index 0000000000000..53f5ddea27cbd --- /dev/null +++ b/app/code/Magento/Customer/Test/Mftf/Metadata/customer_config_account_sharing-meta.xml @@ -0,0 +1,21 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<operations xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:mftf:DataGenerator/etc/dataOperation.xsd"> + <operation name="CustomerAccountShareConfig" dataType="customer_account_sharing_config" type="create" auth="adminFormKey" url="/admin/system_config/save/section/customer/" + successRegex="/messages-message-success/" returnRegex="" method="POST"> + <object key="groups" dataType="customer_account_sharing_config"> + <object key="account_share" dataType="customer_account_sharing_config"> + <object key="fields" dataType="customer_account_sharing_config"> + <object key="scope" dataType="account_share_scope_value"> + <field key="value">string</field> + </object> + </object> + </object> + </object> + </operation> +</operations> \ No newline at end of file diff --git a/app/code/Magento/Customer/Test/Mftf/Page/StorefrontCustomerOrderPage.xml b/app/code/Magento/Customer/Test/Mftf/Page/StorefrontCustomerOrderPage.xml deleted file mode 100644 index 4e2ff7e907aa2..0000000000000 --- a/app/code/Magento/Customer/Test/Mftf/Page/StorefrontCustomerOrderPage.xml +++ /dev/null @@ -1,14 +0,0 @@ -<?xml version="1.0" encoding="UTF-8"?> -<!-- - /** - * Copyright © Magento, Inc. All rights reserved. - * See COPYING.txt for license details. - */ ---> - -<pages xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" - xsi:noNamespaceSchemaLocation="../../../../../../../dev/tests/acceptance/vendor/magento/magento2-functional-testing-framework/src/Magento/FunctionalTestingFramework/Page/etc/PageObject.xsd"> - <page name="StorefrontCustomerOrderPage" url="/sales/order/history/" area="storefront" module="Magento_Customer"> - <section name="StorefrontCustomerOrderViewSection" /> - </page> -</pages> diff --git a/app/code/Magento/Customer/Test/Mftf/Page/StorefrontCustomerOrderViewPage.xml b/app/code/Magento/Customer/Test/Mftf/Page/StorefrontCustomerOrderViewPage.xml index 3ac90876a7acf..9cd6dc1e69e35 100644 --- a/app/code/Magento/Customer/Test/Mftf/Page/StorefrontCustomerOrderViewPage.xml +++ b/app/code/Magento/Customer/Test/Mftf/Page/StorefrontCustomerOrderViewPage.xml @@ -7,8 +7,9 @@ --> <pages xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" - xsi:noNamespaceSchemaLocation="../../../../../../../dev/tests/acceptance/vendor/magento/magento2-functional-testing-framework/src/Magento/FunctionalTestingFramework/Page/etc/PageObject.xsd"> + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Page/etc/PageObject.xsd"> <page name="StorefrontCustomerOrderViewPage" url="sales/order/view/order_id/{{var1}}" area="storefront" module="Magento_Customer" parameterized="true"> <section name="StorefrontCustomerOrderSection" /> + <section name="StorefrontCustomerOrderViewSection" /> </page> -</pages> \ No newline at end of file +</pages> diff --git a/app/code/Magento/Customer/Test/Mftf/Page/StorefrontCustomerOrdersGridPage.xml b/app/code/Magento/Customer/Test/Mftf/Page/StorefrontCustomerOrdersGridPage.xml new file mode 100644 index 0000000000000..effc07d00f408 --- /dev/null +++ b/app/code/Magento/Customer/Test/Mftf/Page/StorefrontCustomerOrdersGridPage.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="StorefrontCustomerOrdersGridPage" url="/sales/order/history/" area="storefront" module="Magento_Customer"> + <section name="StorefrontCustomerOrdersGridSection"/> + </page> +</pages> diff --git a/app/code/Magento/Customer/Test/Mftf/Section/StorefrontCustomerOrdersGridSection.xml b/app/code/Magento/Customer/Test/Mftf/Section/StorefrontCustomerOrdersGridSection.xml new file mode 100644 index 0000000000000..de3975fd82d01 --- /dev/null +++ b/app/code/Magento/Customer/Test/Mftf/Section/StorefrontCustomerOrdersGridSection.xml @@ -0,0 +1,13 @@ +<!-- + /** + * 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="StorefrontCustomerOrdersGridSection"> + <element name="viewOrderAction" type="text" selector="//*[@id='my-orders-table']//*[text()='{{orderId}}']/..//a[contains(@class, 'action view')]" parameterized="true"/> + <element name="orderStatus" type="text" selector="//*[@id='my-orders-table']//*[text()='{{orderId}}']/..//td[contains(@class,'status')]" parameterized="true"/> + </section> +</sections> diff --git a/app/code/Magento/Customer/Test/Mftf/Section/StorefrontPanelHeaderSection.xml b/app/code/Magento/Customer/Test/Mftf/Section/StorefrontPanelHeaderSection.xml index bee3adea898aa..fb5edd35a484d 100644 --- a/app/code/Magento/Customer/Test/Mftf/Section/StorefrontPanelHeaderSection.xml +++ b/app/code/Magento/Customer/Test/Mftf/Section/StorefrontPanelHeaderSection.xml @@ -7,8 +7,13 @@ --> <sections xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" - xsi:noNamespaceSchemaLocation="../../../../../../../dev/tests/acceptance/vendor/magento/magento2-functional-testing-framework/src/Magento/FunctionalTestingFramework/Page/etc/SectionObject.xsd"> + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Page/etc/SectionObject.xsd"> <section name="StorefrontPanelHeaderSection"> <element name="createAnAccountLink" type="select" selector=".panel.header li:nth-child(3)"/> + <element name="customerWelcome" type="text" selector=".panel.header .customer-welcome"/> + <element name="customerWelcomeMenu" type="text" selector=".panel.header .customer-welcome .customer-menu"/> + <element name="customerLogoutLink" type="text" selector=".panel.header .customer-welcome .customer-menu .authorization-link a" timeout="30"/> + <element name="welcomeMessage" type="text" selector=".greet.welcome span"/> + <element name="notYouLink" type="button" selector=".greet.welcome span a"/> </section> </sections> diff --git a/app/code/Magento/Customer/Test/Unit/Model/EmailNotificationTest.php b/app/code/Magento/Customer/Test/Unit/Model/EmailNotificationTest.php index 49a2d5e51f8f8..33757b82db891 100644 --- a/app/code/Magento/Customer/Test/Unit/Model/EmailNotificationTest.php +++ b/app/code/Magento/Customer/Test/Unit/Model/EmailNotificationTest.php @@ -603,23 +603,35 @@ public function testPasswordResetConfirmation() } /** + * @dataProvider emailDataProvider + * @param string $emailType + * @param string $template + * @param string $templateIdentifier + * @param string|null $sendemailStoreId + * @param array $extensions + * + * @return void * @SuppressWarnings(PHPMD.ExcessiveMethodLength) */ - public function testNewAccount() - { + public function testNewAccount( + string $emailType, + string $template, + string $templateIdentifier, + $sendemailStoreId, + array $extensions + ) { $customerId = 1; $customerStoreId = 2; + $customerName = 'Customer Name'; $customerEmail = 'email@email.com'; $customerData = ['key' => 'value']; - $customerName = 'Customer Name'; - $templateIdentifier = 'Template Identifier'; $sender = 'Sender'; - $senderValues = ['name' => $sender, 'email' => $sender]; + $senderEmail = 'sender@sender.com'; + $senderValues = ['name' => $sender, 'email' => $senderEmail]; $this->senderResolverMock ->expects($this->once()) ->method('resolve') - ->with($sender, $customerStoreId) ->willReturn($senderValues); /** @var CustomerInterface | \PHPUnit_Framework_MockObject_MockObject $customer */ @@ -669,23 +681,76 @@ public function testNewAccount() $this->scopeConfigMock->expects($this->at(0)) ->method('getValue') - ->with(EmailNotification::XML_PATH_REGISTER_EMAIL_TEMPLATE, ScopeInterface::SCOPE_STORE, $customerStoreId) + ->with($template, ScopeInterface::SCOPE_STORE, $customerStoreId) ->willReturn($templateIdentifier); + $this->scopeConfigMock->expects($this->at(1)) ->method('getValue') ->with(EmailNotification::XML_PATH_REGISTER_EMAIL_IDENTITY, ScopeInterface::SCOPE_STORE, $customerStoreId) ->willReturn($sender); + $templateVars = [ + 'customer' => $this->customerSecureMock, + 'back_url' => '', + 'store' => $this->storeMock, + ]; + + if ($template === EmailNotification::XML_PATH_CONFIRM_EMAIL_TEMPLATE) { + if (!empty($extensions)) { + $templateVars['url'] = $extensions['url']; + $templateVars['extensions'] = $extensions['extension_info']; + } else { + $templateVars['url'] = EmailNotification::CUSTOMER_CONFIRM_URL; + $templateVars['extensions'] = $extensions; + } + } + $this->mockDefaultTransportBuilder( $templateIdentifier, $customerStoreId, $senderValues, $customerEmail, $customerName, - ['customer' => $this->customerSecureMock, 'back_url' => '', 'store' => $this->storeMock] + $templateVars ); - $this->model->newAccount($customer, EmailNotification::NEW_ACCOUNT_EMAIL_REGISTERED, '', $customerStoreId); + $this->model->newAccount($customer, $emailType, '', $customerStoreId, $sendemailStoreId, $extensions); + } + + /** + * @return array + */ + public function emailDataProvider(): array + { + return + [ + [ + EmailNotification::NEW_ACCOUNT_EMAIL_REGISTERED, + EmailNotification::XML_PATH_REGISTER_EMAIL_TEMPLATE, + 'Register', + null, + [], + ], + [ + EmailNotification::NEW_ACCOUNT_EMAIL_CONFIRMATION, + EmailNotification::XML_PATH_CONFIRM_EMAIL_TEMPLATE, + 'Confirm', + null, + [], + ], + [ + EmailNotification::NEW_ACCOUNT_EMAIL_CONFIRMATION, + EmailNotification::XML_PATH_CONFIRM_EMAIL_TEMPLATE, + 'Confirm', + null, + [ + 'url' => "customer/account/confirm", + 'extension_info' => [ + 'test_extension' => "NTowU0Q5amZneEtSZnRDajRFZFVDVmZCbnhRWnZ0cFFkOA,,", + ], + ], + ], + ]; } /** diff --git a/app/code/Magento/Customer/Test/Unit/Model/Metadata/Form/AbstractDataTest.php b/app/code/Magento/Customer/Test/Unit/Model/Metadata/Form/AbstractDataTest.php index e4dc22ba40e31..667fc87b6a82b 100644 --- a/app/code/Magento/Customer/Test/Unit/Model/Metadata/Form/AbstractDataTest.php +++ b/app/code/Magento/Customer/Test/Unit/Model/Metadata/Form/AbstractDataTest.php @@ -205,6 +205,8 @@ public function applyOutputFilterDataProvider() } /** + * Tests input validation rules. + * * @param null|string $value * @param null|string $label * @param null|string $inputValidation @@ -217,25 +219,18 @@ public function testValidateInputRule($value, $label, $inputValidation, $expecte ->disableOriginalConstructor() ->setMethods(['getName', 'getValue']) ->getMockForAbstractClass(); - $validationRule->expects($this->any()) - ->method('getName') - ->will($this->returnValue('input_validation')); - $validationRule->expects($this->any()) - ->method('getValue') - ->will($this->returnValue($inputValidation)); - - $this->_attributeMock->expects($this->any())->method('getStoreLabel')->will($this->returnValue($label)); - $this->_attributeMock->expects( - $this->any() - )->method( - 'getValidationRules' - )->will( - $this->returnValue( - [ - $validationRule, - ] - ) - ); + + $validationRule->method('getName') + ->willReturn('input_validation'); + + $validationRule->method('getValue') + ->willReturn($inputValidation); + + $this->_attributeMock->method('getStoreLabel') + ->willReturn($label); + + $this->_attributeMock->method('getValidationRules') + ->willReturn([$validationRule]); $this->assertEquals($expectedOutput, $this->_model->validateInputRule($value)); } @@ -256,6 +251,16 @@ public function validateInputRuleDataProvider() \Zend_Validate_Alnum::NOT_ALNUM => '"mylabel" contains non-alphabetic or non-numeric characters.' ] ], + [ + 'abc qaz', + 'mylabel', + 'alphanumeric', + [ + \Zend_Validate_Alnum::NOT_ALNUM => '"mylabel" contains non-alphabetic or non-numeric characters.', + ], + ], + ['abcqaz', 'mylabel', 'alphanumeric', true], + ['abc qaz', 'mylabel', 'alphanum-with-spaces', true], [ '!@#$', 'mylabel', diff --git a/app/code/Magento/Customer/Test/Unit/Model/ResourceModel/CustomerRepositoryTest.php b/app/code/Magento/Customer/Test/Unit/Model/ResourceModel/CustomerRepositoryTest.php index 215cc32193b18..01951fac7172e 100644 --- a/app/code/Magento/Customer/Test/Unit/Model/ResourceModel/CustomerRepositoryTest.php +++ b/app/code/Magento/Customer/Test/Unit/Model/ResourceModel/CustomerRepositoryTest.php @@ -740,7 +740,7 @@ public function testGetList() ->willReturnSelf(); $collection->expects($this->at(7)) ->method('joinAttribute') - ->with('company', 'customer_address/company', 'default_billing', null, 'left') + ->with('billing_company', 'customer_address/company', 'default_billing', null, 'left') ->willReturnSelf(); $this->collectionProcessorMock->expects($this->once()) ->method('process') diff --git a/app/code/Magento/Customer/Test/Unit/Ui/Component/Listing/Column/ValidationRulesTest.php b/app/code/Magento/Customer/Test/Unit/Ui/Component/Listing/Column/ValidationRulesTest.php index 130b3acd11e76..b57bc53ef09f9 100644 --- a/app/code/Magento/Customer/Test/Unit/Ui/Component/Listing/Column/ValidationRulesTest.php +++ b/app/code/Magento/Customer/Test/Unit/Ui/Component/Listing/Column/ValidationRulesTest.php @@ -18,12 +18,6 @@ class ValidationRulesTest extends \PHPUnit\Framework\TestCase protected function setUp() { - $this->validationRules = $this->getMockBuilder( - \Magento\Customer\Ui\Component\Listing\Column\ValidationRules::class - ) - ->disableOriginalConstructor() - ->getMock(); - $this->validationRule = $this->getMockBuilder(\Magento\Customer\Api\Data\ValidationRuleInterface::class) ->disableOriginalConstructor() ->getMock(); @@ -31,18 +25,25 @@ protected function setUp() $this->validationRules = new ValidationRules(); } - public function testGetValidationRules() + /** + * Tests input validation rules. + * + * @param string $validationRule + * @param string $validationClass + * @return void + * @dataProvider validationRulesDataProvider + */ + public function testGetValidationRules(string $validationRule, string $validationClass) { $expectsRules = [ 'required-entry' => true, - 'validate-number' => true, + $validationClass => true, ]; - $this->validationRule->expects($this->atLeastOnce()) - ->method('getName') + $this->validationRule->method('getName') ->willReturn('input_validation'); - $this->validationRule->expects($this->atLeastOnce()) - ->method('getValue') - ->willReturn('numeric'); + + $this->validationRule->method('getValue') + ->willReturn($validationRule); $this->assertEquals( $expectsRules, @@ -66,4 +67,21 @@ public function testGetValidationRulesWithOnlyRequiredRule() $this->validationRules->getValidationRules(true, []) ); } + + /** + * Provides possible validation rules. + * + * @return array + */ + public function validationRulesDataProvider(): array + { + return [ + ['alpha', 'validate-alpha'], + ['numeric', 'validate-number'], + ['alphanumeric', 'validate-alphanum'], + ['alphanum-with-spaces', 'validate-alphanum-with-spaces'], + ['url', 'validate-url'], + ['email', 'validate-email'], + ]; + } } diff --git a/app/code/Magento/Customer/Ui/Component/Listing/Column/ValidationRules.php b/app/code/Magento/Customer/Ui/Component/Listing/Column/ValidationRules.php index b8f83421a6d62..6befec8e942a1 100644 --- a/app/code/Magento/Customer/Ui/Component/Listing/Column/ValidationRules.php +++ b/app/code/Magento/Customer/Ui/Component/Listing/Column/ValidationRules.php @@ -7,6 +7,9 @@ use Magento\Customer\Api\Data\ValidationRuleInterface; +/** + * Provides validation classes according to corresponding rules. + */ class ValidationRules { /** @@ -16,6 +19,7 @@ class ValidationRules 'alpha' => 'validate-alpha', 'numeric' => 'validate-number', 'alphanumeric' => 'validate-alphanum', + 'alphanum-with-spaces' => 'validate-alphanum-with-spaces', 'url' => 'validate-url', 'email' => 'validate-email', ]; diff --git a/app/code/Magento/Customer/etc/di.xml b/app/code/Magento/Customer/etc/di.xml index fa7dd80882632..c6e383fb73bde 100644 --- a/app/code/Magento/Customer/etc/di.xml +++ b/app/code/Magento/Customer/etc/di.xml @@ -127,6 +127,13 @@ <argument name="groupManagement" xsi:type="object">Magento\Customer\Api\GroupManagementInterface\Proxy</argument> </arguments> </type> + <type name="Magento\Customer\Model\Metadata\CustomerMetadata"> + <arguments> + <argument name="systemAttributes" xsi:type="array"> + <item name="disable_auto_group_change" xsi:type="string">disable_auto_group_change</item> + </argument> + </arguments> + </type> <virtualType name="SectionInvalidationConfigReader" type="Magento\Framework\Config\Reader\Filesystem"> <arguments> <argument name="idAttributes" xsi:type="array"> diff --git a/app/code/Magento/Customer/view/frontend/email/account_new_confirmation.html b/app/code/Magento/Customer/view/frontend/email/account_new_confirmation.html index 010087ace2d42..9b183d63471f3 100644 --- a/app/code/Magento/Customer/view/frontend/email/account_new_confirmation.html +++ b/app/code/Magento/Customer/view/frontend/email/account_new_confirmation.html @@ -9,7 +9,9 @@ "var this.getUrl($store, 'customer/account/confirm/', [_query:[id:$customer.id, key:$customer.confirmation, back_url:$back_url]])":"Account Confirmation URL", "var this.getUrl($store, 'customer/account/')":"Customer Account URL", "var customer.email":"Customer Email", -"var customer.name":"Customer Name" +"var customer.name":"Customer Name", +"var extensions":"Extensions", +"var url":"Url" } @--> {{template config_path="design/email/header_template"}} @@ -23,7 +25,7 @@ <table class="inner-wrapper" border="0" cellspacing="0" cellpadding="0" align="center"> <tr> <td align="center"> - <a href="{{var this.getUrl($store,'customer/account/confirm/',[_query:[id:$customer.id,key:$customer.confirmation,back_url:$back_url],_nosid:1])}}" target="_blank">{{trans "Confirm Your Account"}}</a> + <a href="{{var this.getUrl($store,$url,[_query:[id:$customer.id,key:$customer.confirmation,extensions:$extensions,back_url:$back_url],_nosid:1])}}" target="_blank">{{trans "Confirm Your Account"}}</a> </td> </tr> </table> diff --git a/app/code/Magento/Customer/view/frontend/layout/customer_address_form.xml b/app/code/Magento/Customer/view/frontend/layout/customer_address_form.xml index f053805409fe5..f5ee2b347a5b2 100644 --- a/app/code/Magento/Customer/view/frontend/layout/customer_address_form.xml +++ b/app/code/Magento/Customer/view/frontend/layout/customer_address_form.xml @@ -20,6 +20,7 @@ <block class="Magento\Customer\Block\Address\Edit" name="customer_address_edit" template="Magento_Customer::address/edit.phtml" cacheable="false"> <arguments> <argument name="attribute_data" xsi:type="object">Magento\Customer\Block\DataProviders\AddressAttributeData</argument> + <argument name="post_code_config" xsi:type="object">Magento\Customer\Block\DataProviders\PostCodesPatternsAttributeData</argument> </arguments> </block> </referenceContainer> diff --git a/app/code/Magento/Customer/view/frontend/templates/address/edit.phtml b/app/code/Magento/Customer/view/frontend/templates/address/edit.phtml index 644238e3949c5..992c866316d79 100644 --- a/app/code/Magento/Customer/view/frontend/templates/address/edit.phtml +++ b/app/code/Magento/Customer/view/frontend/templates/address/edit.phtml @@ -126,6 +126,9 @@ title="<?= /* @noEscape */ $block->getAttributeData()->getFrontendLabel('postcode') ?>" id="zip" class="input-text validate-zip-international <?= $block->escapeHtmlAttr($this->helper(\Magento\Customer\Helper\Address::class)->getAttributeValidationClass('postcode')) ?>"> + <div role="alert" class="message warning" style="display:none"> + <span></span> + </div> </div> </div> <div class="field country required"> @@ -184,7 +187,9 @@ <script type="text/x-magento-init"> { "#form-validate": { - "addressValidation": {} + "addressValidation": { + "postCodes": <?= /* @noEscape */ $block->getPostCodeConfig()->getSerializedPostCodes(); ?> + } }, "#country": { "regionUpdater": { diff --git a/app/code/Magento/Customer/view/frontend/web/js/addressValidation.js b/app/code/Magento/Customer/view/frontend/web/js/addressValidation.js index be2960701deed..c014b814ea98b 100644 --- a/app/code/Magento/Customer/view/frontend/web/js/addressValidation.js +++ b/app/code/Magento/Customer/view/frontend/web/js/addressValidation.js @@ -5,25 +5,38 @@ define([ 'jquery', + 'underscore', + 'mageUtils', + 'mage/translate', + 'Magento_Checkout/js/model/postcode-validator', 'jquery/ui', 'validation' -], function ($) { +], function ($, __, utils, $t, postCodeValidator) { 'use strict'; $.widget('mage.addressValidation', { options: { selectors: { - button: '[data-action=save-address]' + button: '[data-action=save-address]', + zip: '#zip', + country: 'select[name="country_id"]:visible' } }, + zipInput: null, + countrySelect: null, + /** * Validation creation + * * @protected */ _create: function () { var button = $(this.options.selectors.button, this.element); + this.zipInput = $(this.options.selectors.zip, this.element); + this.countrySelect = $(this.options.selectors.country, this.element); + this.element.validation({ /** @@ -36,6 +49,75 @@ define([ form.submit(); } }); + + this._addPostCodeValidation(); + }, + + /** + * Add postcode validation + * + * @protected + */ + _addPostCodeValidation: function () { + var self = this; + + this.zipInput.on('keyup', __.debounce(function (event) { + var valid = self._validatePostCode(event.target.value); + + self._renderValidationResult(valid); + }, 500) + ); + + this.countrySelect.on('change', function () { + var valid = self._validatePostCode(self.zipInput.val()); + + self._renderValidationResult(valid); + }); + }, + + /** + * Validate post code value. + * + * @protected + * @param {String} postCode - post code + * @return {Boolean} Whether is post code valid + */ + _validatePostCode: function (postCode) { + var countryId = this.countrySelect.val(); + + if (postCode === null) { + return true; + } + + return postCodeValidator.validate(postCode, countryId, this.options.postCodes); + }, + + /** + * Renders warning messages for invalid post code. + * + * @protected + * @param {Boolean} valid + */ + _renderValidationResult: function (valid) { + var warnMessage, + alertDiv = this.zipInput.next(); + + if (!valid) { + warnMessage = $t('Provided Zip/Postal Code seems to be invalid.'); + + if (postCodeValidator.validatedPostCodeExample.length) { + warnMessage += $t(' Example: ') + postCodeValidator.validatedPostCodeExample.join('; ') + '. '; + } + warnMessage += $t('If you believe it is the right one you can ignore this notice.'); + } + + alertDiv.children(':first').text(warnMessage); + + if (valid) { + alertDiv.hide(); + } else { + alertDiv.show(); + } } }); 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 39e3f8d95ee3b..66d37cb84e9cb 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 @@ -216,6 +216,9 @@ define([ $.cookieStorage.set(privateContentVersion, privateContent); } $.localStorage.set(privateContentVersion, privateContent); + _.each(dataProvider.getFromStorage(storage.keys()), function (sectionData, sectionName) { + buffer.notify(sectionName, sectionData); + }); this.reload([], false); isLoading = true; } else if (expiredSectionNames.length > 0) { diff --git a/app/code/Magento/Deploy/Collector/Collector.php b/app/code/Magento/Deploy/Collector/Collector.php index b02c8e478d639..0a71af5758b7d 100644 --- a/app/code/Magento/Deploy/Collector/Collector.php +++ b/app/code/Magento/Deploy/Collector/Collector.php @@ -5,9 +5,11 @@ */ namespace Magento\Deploy\Collector; -use Magento\Deploy\Source\SourcePool; use Magento\Deploy\Package\Package; use Magento\Deploy\Package\PackageFactory; +use Magento\Deploy\Source\SourcePool; +use Magento\Deploy\Package\PackageFile; +use Magento\Framework\Module\Manager; use Magento\Framework\View\Asset\PreProcessor\FileNameResolver; /** @@ -44,6 +46,9 @@ class Collector implements CollectorInterface */ private $packageFactory; + /** @var \Magento\Framework\Module\Manager */ + private $moduleManager; + /** * Default values for package primary identifiers * @@ -61,15 +66,19 @@ class Collector implements CollectorInterface * @param SourcePool $sourcePool * @param FileNameResolver $fileNameResolver * @param PackageFactory $packageFactory + * @param Manager $moduleManager */ public function __construct( SourcePool $sourcePool, FileNameResolver $fileNameResolver, - PackageFactory $packageFactory + PackageFactory $packageFactory, + Manager $moduleManager = null ) { $this->sourcePool = $sourcePool; $this->fileNameResolver = $fileNameResolver; $this->packageFactory = $packageFactory; + $this->moduleManager = $moduleManager ?: \Magento\Framework\App\ObjectManager::getInstance() + ->get(Manager::class); } /** @@ -81,19 +90,11 @@ public function collect() foreach ($this->sourcePool->getAll() as $source) { $files = $source->get(); foreach ($files as $file) { - $file->setDeployedFileName($this->fileNameResolver->resolve($file->getFileName())); - $params = [ - 'area' => $file->getArea(), - 'theme' => $file->getTheme(), - 'locale' => $file->getLocale(), - 'module' => $file->getModule(), - 'isVirtual' => (!$file->getLocale() || !$file->getTheme() || !$file->getArea()) - ]; - foreach ($this->packageDefaultValues as $name => $value) { - if (!isset($params[$name])) { - $params[$name] = $value; - } + if ($file->getModule() && !$this->moduleManager->isEnabled($file->getModule())) { + continue; } + $file->setDeployedFileName($this->fileNameResolver->resolve($file->getFileName())); + $params = $this->getParams($file); $packagePath = "{$params['area']}/{$params['theme']}/{$params['locale']}"; if (!isset($packages[$packagePath])) { $packages[$packagePath] = $this->packageFactory->create($params); @@ -105,4 +106,28 @@ public function collect() } return $packages; } + + /** + * Retrieve package params. + * + * @param PackageFile $file + * @return array + */ + private function getParams(PackageFile $file) + { + $params = [ + 'area' => $file->getArea(), + 'theme' => $file->getTheme(), + 'locale' => $file->getLocale(), + 'module' => $file->getModule(), + 'isVirtual' => (!$file->getLocale() || !$file->getTheme() || !$file->getArea()) + ]; + foreach ($this->packageDefaultValues as $name => $value) { + if (!isset($params[$name])) { + $params[$name] = $value; + } + } + + return $params; + } } diff --git a/app/code/Magento/Directory/Model/Config/Source/WeightUnit.php b/app/code/Magento/Directory/Model/Config/Source/WeightUnit.php index 4e2758b362a43..97d22633af03c 100644 --- a/app/code/Magento/Directory/Model/Config/Source/WeightUnit.php +++ b/app/code/Magento/Directory/Model/Config/Source/WeightUnit.php @@ -3,6 +3,7 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ + namespace Magento\Directory\Model\Config\Source; /** @@ -14,10 +15,23 @@ class WeightUnit implements \Magento\Framework\Option\ArrayInterface { /** - * {@inheritdoc} + * @var string + */ + const CODE_LBS = 'lbs'; + + /** + * @var string + */ + const CODE_KGS = 'kgs'; + + /** + * @inheritdoc */ public function toOptionArray() { - return [['value' => 'lbs', 'label' => __('lbs')], ['value' => 'kgs', 'label' => __('kgs')]]; + return [ + ['value' => self::CODE_LBS, 'label' => __('lbs')], + ['value' => self::CODE_KGS, 'label' => __('kgs')] + ]; } } diff --git a/app/code/Magento/Eav/Model/Attribute/Data/AbstractData.php b/app/code/Magento/Eav/Model/Attribute/Data/AbstractData.php index 12023acc3b33b..f4bcdf7764d95 100644 --- a/app/code/Magento/Eav/Model/Attribute/Data/AbstractData.php +++ b/app/code/Magento/Eav/Model/Attribute/Data/AbstractData.php @@ -144,7 +144,8 @@ public function setRequestScope($scope) } /** - * Set scope visibility + * Set scope visibility. + * * Search value only in scope or search value in scope and global * * @param bool $flag @@ -313,9 +314,13 @@ protected function _validateInputRule($value) if (!empty($validateRules['input_validation'])) { $label = $this->getAttribute()->getStoreLabel(); + $allowWhiteSpace = false; switch ($validateRules['input_validation']) { + case 'alphanum-with-spaces': + $allowWhiteSpace = true; + // continue to alphanumeric validation case 'alphanumeric': - $validator = new \Zend_Validate_Alnum(true); + $validator = new \Zend_Validate_Alnum($allowWhiteSpace); $validator->setMessage(__('"%1" invalid type entered.', $label), \Zend_Validate_Alnum::INVALID); $validator->setMessage( __('"%1" contains non-alphabetic or non-numeric characters.', $label), diff --git a/app/code/Magento/Eav/Model/Entity/Attribute/Frontend/AbstractFrontend.php b/app/code/Magento/Eav/Model/Entity/Attribute/Frontend/AbstractFrontend.php index 13c0b7a627edb..cd7639080509f 100644 --- a/app/code/Magento/Eav/Model/Entity/Attribute/Frontend/AbstractFrontend.php +++ b/app/code/Magento/Eav/Model/Entity/Attribute/Frontend/AbstractFrontend.php @@ -20,6 +20,8 @@ use Magento\Eav\Model\Entity\Attribute\Source\BooleanFactory; /** + * EAV entity attribute form renderer. + * * @api * @since 100.0.2 */ @@ -234,6 +236,9 @@ protected function _getInputValidateClass() case 'alphanumeric': $class = 'validate-alphanum'; break; + case 'alphanum-with-spaces': + $class = 'validate-alphanum-with-spaces'; + break; case 'numeric': $class = 'validate-digits'; break; diff --git a/app/code/Magento/Eav/Model/Entity/Collection/AbstractCollection.php b/app/code/Magento/Eav/Model/Entity/Collection/AbstractCollection.php index 2177686a4a6be..7ac77a66482b8 100644 --- a/app/code/Magento/Eav/Model/Entity/Collection/AbstractCollection.php +++ b/app/code/Magento/Eav/Model/Entity/Collection/AbstractCollection.php @@ -394,7 +394,7 @@ public function addAttributeToFilter($attribute, $condition = null, $joinType = */ public function addFieldToFilter($attribute, $condition = null) { - return $this->addAttributeToFilter($attribute, $condition); + return $this->addAttributeToFilter($attribute, $condition, 'left'); } /** diff --git a/app/code/Magento/Eav/Test/Unit/Model/Attribute/Data/TextTest.php b/app/code/Magento/Eav/Test/Unit/Model/Attribute/Data/TextTest.php index 217a04045b939..8b16d286471b4 100644 --- a/app/code/Magento/Eav/Test/Unit/Model/Attribute/Data/TextTest.php +++ b/app/code/Magento/Eav/Test/Unit/Model/Attribute/Data/TextTest.php @@ -6,22 +6,24 @@ namespace Magento\Eav\Test\Unit\Model\Attribute\Data; +use Magento\Framework\Stdlib\StringUtils; + class TextTest extends \PHPUnit\Framework\TestCase { /** * @var \Magento\Eav\Model\Attribute\Data\Text */ - protected $_model; + private $model; protected function setUp() { $locale = $this->createMock(\Magento\Framework\Stdlib\DateTime\TimezoneInterface::class); $localeResolver = $this->createMock(\Magento\Framework\Locale\ResolverInterface::class); $logger = $this->createMock(\Psr\Log\LoggerInterface::class); - $helper = $this->createMock(\Magento\Framework\Stdlib\StringUtils::class); + $helper = new StringUtils; - $this->_model = new \Magento\Eav\Model\Attribute\Data\Text($locale, $logger, $localeResolver, $helper); - $this->_model->setAttribute( + $this->model = new \Magento\Eav\Model\Attribute\Data\Text($locale, $logger, $localeResolver, $helper); + $this->model->setAttribute( $this->createAttribute( [ 'store_label' => 'Test', @@ -35,24 +37,39 @@ protected function setUp() protected function tearDown() { - $this->_model = null; + $this->model = null; } + /** + * Test of string validation. + * + * @return void + */ public function testValidateValueString() { $inputValue = '0'; $expectedResult = true; - $this->assertEquals($expectedResult, $this->_model->validateValue($inputValue)); + $this->assertEquals($expectedResult, $this->model->validateValue($inputValue)); } + /** + * Test of integer validation. + * + * @return void + */ public function testValidateValueInteger() { $inputValue = 0; $expectedResult = ['"Test" is a required value.']; - $result = $this->_model->validateValue($inputValue); + $result = $this->model->validateValue($inputValue); $this->assertEquals($expectedResult, [(string)$result[0]]); } + /** + * Test without length validation. + * + * @return void + */ public function testWithoutLengthValidation() { $expectedResult = true; @@ -64,12 +81,109 @@ public function testWithoutLengthValidation() ]; $defaultAttributeData['validate_rules']['min_text_length'] = 2; - $this->_model->setAttribute($this->createAttribute($defaultAttributeData)); - $this->assertEquals($expectedResult, $this->_model->validateValue('t')); + $this->model->setAttribute($this->createAttribute($defaultAttributeData)); + $this->assertEquals($expectedResult, $this->model->validateValue('t')); $defaultAttributeData['validate_rules']['max_text_length'] = 3; - $this->_model->setAttribute($this->createAttribute($defaultAttributeData)); - $this->assertEquals($expectedResult, $this->_model->validateValue('test')); + $this->model->setAttribute($this->createAttribute($defaultAttributeData)); + $this->assertEquals($expectedResult, $this->model->validateValue('test')); + } + + /** + * Test of alphanumeric validation. + * + * @param string $value + * @param bool|array $expectedResult + * @return void + * @dataProvider alphanumDataProvider + */ + public function testAlphanumericValidation(string $value, $expectedResult) + { + $defaultAttributeData = [ + 'store_label' => 'Test', + 'attribute_code' => 'test', + 'is_required' => 1, + 'validate_rules' => [ + 'min_text_length' => 0, + 'max_text_length' => 10, + 'input_validation' => 'alphanumeric', + ], + ]; + + $this->model->setAttribute($this->createAttribute($defaultAttributeData)); + $this->assertEquals($expectedResult, $this->model->validateValue($value)); + } + + /** + * Provides possible input values. + * + * @return array + */ + public function alphanumDataProvider(): array + { + return [ + ['QazWsx', true], + ['QazWsx123', true], + [ + 'QazWsx 123', + [\Zend_Validate_Alnum::NOT_ALNUM => '"Test" contains non-alphabetic or non-numeric characters.'], + ], + [ + 'QazWsx_123', + [\Zend_Validate_Alnum::NOT_ALNUM => '"Test" contains non-alphabetic or non-numeric characters.'], + ], + [ + 'QazWsx12345', + [__('"%1" length must be equal or less than %2 characters.', 'Test', 10)], + ], + ]; + } + + /** + * Test of alphanumeric validation with spaces. + * + * @param string $value + * @param bool|array $expectedResult + * @return void + * @dataProvider alphanumWithSpacesDataProvider + */ + public function testAlphanumericValidationWithSpaces(string $value, $expectedResult) + { + $defaultAttributeData = [ + 'store_label' => 'Test', + 'attribute_code' => 'test', + 'is_required' => 1, + 'validate_rules' => [ + 'min_text_length' => 0, + 'max_text_length' => 10, + 'input_validation' => 'alphanum-with-spaces', + ], + ]; + + $this->model->setAttribute($this->createAttribute($defaultAttributeData)); + $this->assertEquals($expectedResult, $this->model->validateValue($value)); + } + + /** + * Provides possible input values. + * + * @return array + */ + public function alphanumWithSpacesDataProvider(): array + { + return [ + ['QazWsx', true], + ['QazWsx123', true], + ['QazWsx 123', true], + [ + 'QazWsx_123', + [\Zend_Validate_Alnum::NOT_ALNUM => '"Test" contains non-alphabetic or non-numeric characters.'], + ], + [ + 'QazWsx12345', + [__('"%1" length must be equal or less than %2 characters.', 'Test', 10)], + ], + ]; } /** diff --git a/app/code/Magento/Eav/Test/Unit/Model/Entity/Attribute/Frontend/DefaultFrontendTest.php b/app/code/Magento/Eav/Test/Unit/Model/Entity/Attribute/Frontend/DefaultFrontendTest.php index a61c9ef447458..2c5b1aeb7bbfd 100644 --- a/app/code/Magento/Eav/Test/Unit/Model/Entity/Attribute/Frontend/DefaultFrontendTest.php +++ b/app/code/Magento/Eav/Test/Unit/Model/Entity/Attribute/Frontend/DefaultFrontendTest.php @@ -13,41 +13,42 @@ use Magento\Framework\App\CacheInterface; use Magento\Eav\Model\Entity\Attribute\AbstractAttribute; use Magento\Eav\Model\Entity\Attribute\Source\AbstractSource; +use PHPUnit\Framework\MockObject\MockObject; class DefaultFrontendTest extends \PHPUnit\Framework\TestCase { /** * @var DefaultFrontend */ - protected $model; + private $model; /** - * @var BooleanFactory|\PHPUnit_Framework_MockObject_MockObject + * @var BooleanFactory|MockObject */ - protected $booleanFactory; + private $booleanFactory; /** - * @var Serializer|\PHPUnit_Framework_MockObject_MockObject + * @var Serializer|MockObject */ private $serializerMock; /** - * @var StoreManagerInterface|\PHPUnit_Framework_MockObject_MockObject + * @var StoreManagerInterface|MockObject */ private $storeManagerMock; /** - * @var StoreInterface|\PHPUnit_Framework_MockObject_MockObject + * @var StoreInterface|MockObject */ private $storeMock; /** - * @var CacheInterface|\PHPUnit_Framework_MockObject_MockObject + * @var CacheInterface|MockObject */ private $cacheMock; /** - * @var AbstractAttribute|\PHPUnit_Framework_MockObject_MockObject + * @var AbstractAttribute|MockObject */ private $attributeMock; @@ -57,7 +58,7 @@ class DefaultFrontendTest extends \PHPUnit\Framework\TestCase private $cacheTags; /** - * @var AbstractSource|\PHPUnit_Framework_MockObject_MockObject + * @var AbstractSource|MockObject */ private $sourceMock; @@ -76,44 +77,31 @@ protected function setUp() ->getMockForAbstractClass(); $this->cacheMock = $this->getMockBuilder(CacheInterface::class) ->getMockForAbstractClass(); - $this->attributeMock = $this->getMockBuilder(AbstractAttribute::class) - ->disableOriginalConstructor() - ->setMethods(['getAttributeCode', 'getSource']) - ->getMockForAbstractClass(); + $this->attributeMock = $this->createAttributeMock(); $this->sourceMock = $this->getMockBuilder(AbstractSource::class) ->disableOriginalConstructor() ->setMethods(['getAllOptions']) ->getMockForAbstractClass(); - $objectManager = new \Magento\Framework\TestFramework\Unit\Helper\ObjectManager($this); - $this->model = $objectManager->getObject( - DefaultFrontend::class, - [ - '_attrBooleanFactory' => $this->booleanFactory, - 'cache' => $this->cacheMock, - 'storeManager' => $this->storeManagerMock, - 'serializer' => $this->serializerMock, - '_attribute' => $this->attributeMock, - 'cacheTags' => $this->cacheTags - ] + $this->model = new DefaultFrontend( + $this->booleanFactory, + $this->cacheMock, + null, + $this->cacheTags, + $this->storeManagerMock, + $this->serializerMock ); + + $this->model->setAttribute($this->attributeMock); } public function testGetClassEmpty() { - $attributeMock = $this->getMockBuilder(\Magento\Eav\Model\Entity\Attribute\AbstractAttribute::class) - ->disableOriginalConstructor() - ->setMethods([ - 'getIsRequired', - 'getFrontendClass', - 'getValidateRules', - ]) - ->getMock(); - $attributeMock->expects($this->once()) - ->method('getIsRequired') + /** @var AbstractAttribute|MockObject $attributeMock */ + $attributeMock = $this->createAttributeMock(); + $attributeMock->method('getIsRequired') ->willReturn(false); - $attributeMock->expects($this->once()) - ->method('getFrontendClass') + $attributeMock->method('getFrontendClass') ->willReturn(''); $attributeMock->expects($this->exactly(2)) ->method('getValidateRules') @@ -123,26 +111,26 @@ public function testGetClassEmpty() $this->assertEmpty($this->model->getClass()); } - public function testGetClass() + /** + * Validates generated html classes. + * + * @param string $validationRule + * @param string $expectedClass + * @return void + * @dataProvider validationRulesDataProvider + */ + public function testGetClass(string $validationRule, string $expectedClass) { - $attributeMock = $this->getMockBuilder(\Magento\Eav\Model\Entity\Attribute\AbstractAttribute::class) - ->disableOriginalConstructor() - ->setMethods([ - 'getIsRequired', - 'getFrontendClass', - 'getValidateRules', - ]) - ->getMock(); - $attributeMock->expects($this->once()) - ->method('getIsRequired') + /** @var AbstractAttribute|MockObject $attributeMock */ + $attributeMock = $this->createAttributeMock(); + $attributeMock->method('getIsRequired') ->willReturn(true); - $attributeMock->expects($this->once()) - ->method('getFrontendClass') + $attributeMock->method('getFrontendClass') ->willReturn(''); $attributeMock->expects($this->exactly(3)) ->method('getValidateRules') ->willReturn([ - 'input_validation' => 'alphanumeric', + 'input_validation' => $validationRule, 'min_text_length' => 1, 'max_text_length' => 2, ]); @@ -150,7 +138,7 @@ public function testGetClass() $this->model->setAttribute($attributeMock); $result = $this->model->getClass(); - $this->assertContains('validate-alphanum', $result); + $this->assertContains($expectedClass, $result); $this->assertContains('minimum-length-1', $result); $this->assertContains('maximum-length-2', $result); $this->assertContains('validate-length', $result); @@ -158,19 +146,11 @@ public function testGetClass() public function testGetClassLength() { - $attributeMock = $this->getMockBuilder(\Magento\Eav\Model\Entity\Attribute\AbstractAttribute::class) - ->disableOriginalConstructor() - ->setMethods([ - 'getIsRequired', - 'getFrontendClass', - 'getValidateRules', - ]) - ->getMock(); - $attributeMock->expects($this->once()) - ->method('getIsRequired') + /** @var AbstractAttribute|MockObject $attributeMock */ + $attributeMock = $this->createAttributeMock(); + $attributeMock->method('getIsRequired') ->willReturn(true); - $attributeMock->expects($this->once()) - ->method('getFrontendClass') + $attributeMock->method('getFrontendClass') ->willReturn(''); $attributeMock->expects($this->exactly(3)) ->method('getValidateRules') @@ -196,33 +176,62 @@ public function testGetSelectOptions() $options = ['option1', 'option2']; $serializedOptions = "{['option1', 'option2']}"; - $this->storeManagerMock->expects($this->once()) - ->method('getStore') + $this->storeManagerMock->method('getStore') ->willReturn($this->storeMock); - $this->storeMock->expects($this->once()) - ->method('getId') + $this->storeMock->method('getId') ->willReturn($storeId); - $this->attributeMock->expects($this->once()) - ->method('getAttributeCode') + $this->attributeMock->method('getAttributeCode') ->willReturn($attributeCode); - $this->cacheMock->expects($this->once()) - ->method('load') + $this->cacheMock->method('load') ->with($cacheKey) ->willReturn(false); - $this->attributeMock->expects($this->once()) - ->method('getSource') + $this->attributeMock->method('getSource') ->willReturn($this->sourceMock); - $this->sourceMock->expects($this->once()) - ->method('getAllOptions') + $this->sourceMock->method('getAllOptions') ->willReturn($options); - $this->serializerMock->expects($this->once()) - ->method('serialize') + $this->serializerMock->method('serialize') ->with($options) ->willReturn($serializedOptions); - $this->cacheMock->expects($this->once()) - ->method('save') + $this->cacheMock->method('save') ->with($serializedOptions, $cacheKey, $this->cacheTags); $this->assertSame($options, $this->model->getSelectOptions()); } + + /** + * Provides possible validation types. + * + * @return array + */ + public function validationRulesDataProvider(): array + { + return [ + ['alphanumeric', 'validate-alphanum'], + ['alphanum-with-spaces', 'validate-alphanum-with-spaces'], + ['alpha', 'validate-alpha'], + ['numeric', 'validate-digits'], + ['url', 'validate-url'], + ['email', 'validate-email'], + ['length', 'validate-length'], + ]; + } + + /** + * Entity attribute factory. + * + * @return AbstractAttribute|MockObject + */ + private function createAttributeMock() + { + return $this->getMockBuilder(AbstractAttribute::class) + ->disableOriginalConstructor() + ->setMethods([ + 'getIsRequired', + 'getFrontendClass', + 'getValidateRules', + 'getAttributeCode', + 'getSource' + ]) + ->getMockForAbstractClass(); + } } diff --git a/app/code/Magento/GiftMessage/Block/Message/Inline.php b/app/code/Magento/GiftMessage/Block/Message/Inline.php index 9f68199106cc7..b3b690710ec47 100644 --- a/app/code/Magento/GiftMessage/Block/Message/Inline.php +++ b/app/code/Magento/GiftMessage/Block/Message/Inline.php @@ -139,7 +139,7 @@ public function getType() /** * Define checkout type * - * @param $type string + * @param string $type * @return $this * @codeCoverageIgnore */ @@ -238,7 +238,7 @@ public function getMessage($entity = null) */ public function getItems() { - if (!$this->getData('items')) { + if (!$this->hasData('items')) { $items = []; $entityItems = $this->getEntity()->getAllItems(); @@ -277,13 +277,24 @@ public function countItems() return count($this->getItems()); } + /** + * Call method getItemsHasMessages. + * + * @deprecated Misspelled method + * @see getItemsHasMessages + */ + public function getItemsHasMesssages() + { + return $this->getItemsHasMessages(); + } + /** * Check if items has messages * * @return bool * @SuppressWarnings(PHPMD.BooleanGetMethodName) */ - public function getItemsHasMesssages() + public function getItemsHasMessages() { foreach ($this->getItems() as $item) { if ($item->getGiftMessageId()) { @@ -316,6 +327,21 @@ public function getEscaped($value, $defaultValue = '') return $this->escapeHtml(trim($value) != '' ? $value : $defaultValue); } + /** + * Check availability of order level functionality. + * + * @return bool|null + */ + public function isMessagesOrderAvailable() + { + $entity = $this->getEntity(); + if (!$entity->hasIsGiftOptionsAvailable()) { + $this->_eventManager->dispatch('gift_options_prepare', ['entity' => $entity]); + } + + return $entity->getIsGiftOptionsAvailable(); + } + /** * Check availability of giftmessages on order level * @@ -346,7 +372,7 @@ public function isItemMessagesAvailable($item) protected function _toHtml() { // render HTML when messages are allowed for order or for items only - if ($this->isItemsAvailable() || $this->isMessagesAvailable()) { + if ($this->isItemsAvailable() || $this->isMessagesAvailable() || $this->isMessagesOrderAvailable()) { return parent::_toHtml(); } return ''; diff --git a/app/code/Magento/GiftMessage/Test/Mftf/Data/GiftOptionsData.xml b/app/code/Magento/GiftMessage/Test/Mftf/Data/GiftOptionsData.xml new file mode 100644 index 0000000000000..bb3f566f1cbc6 --- /dev/null +++ b/app/code/Magento/GiftMessage/Test/Mftf/Data/GiftOptionsData.xml @@ -0,0 +1,27 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<entities xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:DataGenerator/etc/dataProfileSchema.xsd"> + <entity name="EnableGiftMessageForOrder" type="gift_message_config_state"> + <requiredEntity type="allow_order">AllowGiftMessageForOrder</requiredEntity> + </entity> + <entity name="AllowGiftMessageForOrder" type="allow_order"> + <data key="value">1</data> + </entity> + <entity name="DefaultConfigGiftMessageOptions" type="gift_message_config_state"> + <requiredEntity type="allow_order">OrderDefault</requiredEntity> + <requiredEntity type="allow_items">ItemsDefault</requiredEntity> + </entity> + <entity name="OrderDefault" type="allow_order"> + <data key="value">0</data> + </entity> + <entity name="ItemsDefault" type="allow_items"> + <data key="value">0</data> + </entity> +</entities> diff --git a/app/code/Magento/GiftMessage/Test/Mftf/Metadata/gift_options-meta.xml b/app/code/Magento/GiftMessage/Test/Mftf/Metadata/gift_options-meta.xml new file mode 100644 index 0000000000000..d4b7997e77d15 --- /dev/null +++ b/app/code/Magento/GiftMessage/Test/Mftf/Metadata/gift_options-meta.xml @@ -0,0 +1,24 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<operations xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:DataGenerator/etc/dataOperation.xsd"> + <operation name="SalesGiftMessageConfigState" dataType="gift_message_config_state" type="create" auth="adminFormKey" url="/admin/system_config/save/section/sales/" method="POST" successRegex="/messages-message-success/"> + <object key="groups" dataType="gift_message_config_state"> + <object key="gift_options" dataType="gift_message_config_state"> + <object key="fields" dataType="gift_message_config_state"> + <object key="allow_order" dataType="allow_order"> + <field key="value">string</field> + </object> + <object key="allow_items" dataType="allow_items"> + <field key="value">string</field> + </object> + </object> + </object> + </object> + </operation> +</operations> diff --git a/app/code/Magento/GiftMessage/Test/Mftf/Section/StorefrontCheckoutGiftOptionsSection.xml b/app/code/Magento/GiftMessage/Test/Mftf/Section/StorefrontCheckoutGiftOptionsSection.xml new file mode 100644 index 0000000000000..f80237fc28423 --- /dev/null +++ b/app/code/Magento/GiftMessage/Test/Mftf/Section/StorefrontCheckoutGiftOptionsSection.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="StorefrontCheckoutGiftOptionsSection"> + <element name="addGiftOptionsCheckbox" type="checkbox" + selector="//span[text()='Add Gift Options']/../../input[@class='checkbox']" timeout="30"/> + </section> +</sections> diff --git a/app/code/Magento/GiftMessage/view/frontend/templates/inline.phtml b/app/code/Magento/GiftMessage/view/frontend/templates/inline.phtml index dec54cfeb9df9..640ef1ba16486 100644 --- a/app/code/Magento/GiftMessage/view/frontend/templates/inline.phtml +++ b/app/code/Magento/GiftMessage/view/frontend/templates/inline.phtml @@ -152,6 +152,7 @@ </div> <dl class="options-items" id="allow-gift-options-container-<?= /* @escapeNotVerified */ $block->getEntity()->getId() ?>"> + <?php if ($block->isMessagesOrderAvailable() || $block->isMessagesAvailable()): ?> <dt id="add-gift-options-for-order-<?= /* @escapeNotVerified */ $block->getEntity()->getId() ?>" class="order-title"> <div class="field choice"> <input type="checkbox" name="allow_gift_options_for_order_<?= /* @escapeNotVerified */ $block->getEntity()->getId() ?>" id="allow_gift_options_for_order_<?= /* @escapeNotVerified */ $block->getEntity()->getId() ?>" data-mage-init='{"giftOptions":{}}' value="1" data-selector='{"id":"#allow-gift-options-for-order-container-<?= /* @escapeNotVerified */ $block->getEntity()->getId() ?>"}'<?php if ($block->getEntityHasMessage()): ?> checked="checked"<?php endif; ?> class="checkbox" /> @@ -192,7 +193,7 @@ </div> <?php endif; ?> </dd> - + <?php endif; ?> <?php if ($block->isItemsAvailable()): ?> <dt id="add-gift-options-for-items-<?= /* @escapeNotVerified */ $block->getEntity()->getId() ?>" class="order-title individual"> <div class="field choice"> diff --git a/app/code/Magento/GroupedProduct/Test/Mftf/ActionGroup/AdminGroupedProductActionGroup.xml b/app/code/Magento/GroupedProduct/Test/Mftf/ActionGroup/AdminGroupedProductActionGroup.xml new file mode 100644 index 0000000000000..b827115f6e066 --- /dev/null +++ b/app/code/Magento/GroupedProduct/Test/Mftf/ActionGroup/AdminGroupedProductActionGroup.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="AdminAssignProductToGroup"> + <arguments> + <argument name="product"/> + </arguments> + + <scrollTo selector="{{AdminProductFormGroupedProductsSection.toggleGroupedProduct}}" x="0" y="-100" stepKey="scrollToGroupedSection"/> + <conditionalClick selector="{{AdminProductFormGroupedProductsSection.toggleGroupedProduct}}" dependentSelector="{{AdminProductFormGroupedProductsSection.addProductsToGroup}}" visible="false" stepKey="openGroupedProductsSection"/> + <click selector="{{AdminProductFormGroupedProductsSection.addProductsToGroup}}" stepKey="clickAddProductsToGroup"/> + <conditionalClick selector="{{AdminAddProductsToGroupPanelSection.clearFilters}}" dependentSelector="{{AdminAddProductsToGroupPanelSection.clearFilters}}" visible="true" stepKey="clearExistingFilters"/> + <click selector="{{AdminAddProductsToGroupPanelSection.filters}}" stepKey="showFiltersPanel"/> + <fillField userInput="{{product.name}}" selector="{{AdminAddProductsToGroupPanelSection.nameFilter}}" stepKey="fillNameFilter"/> + <click selector="{{AdminAddProductsToGroupPanelSection.applyFilters}}" stepKey="clickApplyFilters"/> + <click selector="{{AdminAddProductsToGroupPanelSection.firstCheckbox}}" stepKey="selectProduct"/> + <click selector="{{AdminAddProductsToGroupPanelSection.addSelectedProducts}}" stepKey="clickAddSelectedGroupProducts"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/GroupedProduct/Test/Mftf/Data/GroupedProductData.xml b/app/code/Magento/GroupedProduct/Test/Mftf/Data/GroupedProductData.xml index e760b877fa33d..4d979953a934e 100644 --- a/app/code/Magento/GroupedProduct/Test/Mftf/Data/GroupedProductData.xml +++ b/app/code/Magento/GroupedProduct/Test/Mftf/Data/GroupedProductData.xml @@ -8,6 +8,15 @@ <entities xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:mftf:DataGenerator/etc/dataProfileSchema.xsd"> + <entity name="GroupedProduct" type="product"> + <data key="sku" unique="suffix">groupedproduct</data> + <data key="type_id">grouped</data> + <data key="attribute_set_id">4</data> + <data key="name" unique="suffix">GroupedProduct</data> + <data key="status">1</data> + <data key="urlKey" unique="suffix">groupedproduct</data> + <requiredEntity type="product_extension_attribute">EavStockItem</requiredEntity> + </entity> <entity name="ApiGroupedProduct" type="product3"> <data key="sku" unique="suffix">api-grouped-product</data> <data key="type_id">grouped</data> diff --git a/app/code/Magento/GroupedProduct/Test/Mftf/Page/AdminOrderCreatePage.xml b/app/code/Magento/GroupedProduct/Test/Mftf/Page/AdminOrderCreatePage.xml new file mode 100644 index 0000000000000..d9c6c4023c07d --- /dev/null +++ b/app/code/Magento/GroupedProduct/Test/Mftf/Page/AdminOrderCreatePage.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="AdminOrderCreatePage" url="sales/order_create/index" area="admin" module="Magento_Sales"> + <section name="AdminOrderConfigureGroupedProductSection"/> + </page> +</pages> diff --git a/app/code/Magento/GroupedProduct/Test/Mftf/Page/AdminProductCreatePage.xml b/app/code/Magento/GroupedProduct/Test/Mftf/Page/AdminProductCreatePage.xml new file mode 100644 index 0000000000000..4fd214111de93 --- /dev/null +++ b/app/code/Magento/GroupedProduct/Test/Mftf/Page/AdminProductCreatePage.xml @@ -0,0 +1,15 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<pages xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Page/etc/PageObject.xsd"> + <page name="AdminProductCreatePage" url="catalog/product/new/set/{{set}}/type/{{type}}/" area="admin" module="Magento_Catalog" parameterized="true"> + <section name="AdminAddProductsToGroupPanelSection"/> + <section name="AdminProductFormGroupedProductsSection"/> + </page> +</pages> diff --git a/app/code/Magento/GroupedProduct/Test/Mftf/Section/AdminAddProductsToGroupPanelSection.xml b/app/code/Magento/GroupedProduct/Test/Mftf/Section/AdminAddProductsToGroupPanelSection.xml new file mode 100644 index 0000000000000..a0de10e580dea --- /dev/null +++ b/app/code/Magento/GroupedProduct/Test/Mftf/Section/AdminAddProductsToGroupPanelSection.xml @@ -0,0 +1,19 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<sections xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Page/etc/SectionObject.xsd"> + <section name="AdminAddProductsToGroupPanelSection"> + <element name="addSelectedProducts" type="button" selector=".product_form_product_form_grouped_grouped_products_modal button.action-primary" timeout="30"/> + <element name="clearFilters" type="button" selector=".product_form_product_form_grouped_grouped_products_modal [data-action='grid-filter-reset']" timeout="30"/> + <element name="filters" type="button" selector=".product_form_product_form_grouped_grouped_products_modal [data-action='grid-filter-expand']" timeout="30"/> + <element name="applyFilters" type="button" selector=".product_form_product_form_grouped_grouped_products_modal [data-action='grid-filter-apply']" timeout="30"/> + <element name="nameFilter" type="input" selector=".product_form_product_form_grouped_grouped_products_modal [name='name']"/> + <element name="firstCheckbox" type="input" selector=".product_form_product_form_grouped_grouped_products_modal tr[data-repeat-index='0'] .admin__control-checkbox"/> + </section> +</sections> diff --git a/app/code/Magento/GroupedProduct/Test/Mftf/Section/AdminOrderConfigureGroupedProductSection.xml b/app/code/Magento/GroupedProduct/Test/Mftf/Section/AdminOrderConfigureGroupedProductSection.xml new file mode 100644 index 0000000000000..04f9f68a27589 --- /dev/null +++ b/app/code/Magento/GroupedProduct/Test/Mftf/Section/AdminOrderConfigureGroupedProductSection.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="AdminOrderConfigureGroupedProductSection"> + <element name="configureProductOk" type="button" selector="[data-role='modal']._show [data-role='action']" timeout="30"/> + <element name="configureProductQtyField" type="input" selector="#super-product-table tr:nth-of-type({{index}}) td.col-qty input.qty" parameterized="true"/> + </section> +</sections> diff --git a/app/code/Magento/GroupedProduct/Test/Mftf/Section/AdminProductFormGroupedProductsSection.xml b/app/code/Magento/GroupedProduct/Test/Mftf/Section/AdminProductFormGroupedProductsSection.xml new file mode 100644 index 0000000000000..5d9e35adca664 --- /dev/null +++ b/app/code/Magento/GroupedProduct/Test/Mftf/Section/AdminProductFormGroupedProductsSection.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="AdminProductFormGroupedProductsSection"> + <element name="toggleGroupedProduct" type="button" selector="[data-index=grouped] .admin__collapsible-title"/> + <element name="addProductsToGroup" type="button" selector="[data-index=grouped] button[data-index='grouped_products_button']" timeout="30"/> + <element name="nextActionButton" type="button" selector="[data-index='grouped'] .action-next"/> + <element name="previousActionButton" type="button" selector="[data-index='grouped'] .action-previous"/> + <element name="positionProduct" type="input" selector="[data-index='grouped'] tr[class*='data-row']:nth-child({{arg}}) td[data-index='positionCalculated'] .position-widget-input" parameterized="true"/> + <element name="nameProductFromGrid" type="text" selector="[data-index='grouped'] tr[class*='data-row']:nth-child({{arg}}) td[data-index='name'] .admin__field-control span" parameterized="true"/> + <element name="buttonContainer" type="block" selector="[data-index='grouped_products_button_set']"/> + </section> +</sections> diff --git a/app/code/Magento/GroupedProduct/Test/Mftf/Test/AdminSortingAssociatedProductsTest.xml b/app/code/Magento/GroupedProduct/Test/Mftf/Test/AdminSortingAssociatedProductsTest.xml new file mode 100644 index 0000000000000..d1e1a36285890 --- /dev/null +++ b/app/code/Magento/GroupedProduct/Test/Mftf/Test/AdminSortingAssociatedProductsTest.xml @@ -0,0 +1,209 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> + <test name="AdminSortingAssociatedProductsTest"> + <annotations> + <features value="GroupedProduct"/> + <stories value="MAGETWO-90189: Grouped Products: Associated Products Can't Be Sorted Between Pages"/> + <title value="Grouped Products: Sorting Associated Products Between Pages"/> + <description value="Make sure that products in grid were recalculated when sorting associated products between pages"/> + <severity value="MAJOR"/> + <testCaseId value="MAGETWO-96474"/> + <group value="groupedProduct"/> + </annotations> + <before> + <actionGroup ref="LoginAsAdmin" stepKey="login"/> + <createData entity="_defaultCategory" stepKey="createCategory"/> + <!-- Create 23 products so that grid can have more than one page --> + <createData entity="ApiSimpleProduct" stepKey="createProduct"> + <requiredEntity createDataKey="createCategory"/> + </createData> + <createData entity="ApiSimpleProduct" stepKey="createProduct1"> + <requiredEntity createDataKey="createCategory"/> + </createData> + <createData entity="ApiSimpleProduct" stepKey="createProduct2"> + <requiredEntity createDataKey="createCategory"/> + </createData> + <createData entity="ApiSimpleProduct" stepKey="createProduct3"> + <requiredEntity createDataKey="createCategory"/> + </createData> + <createData entity="ApiSimpleProduct" stepKey="createProduct4"> + <requiredEntity createDataKey="createCategory"/> + </createData> + <createData entity="ApiSimpleProduct" stepKey="createProduct5"> + <requiredEntity createDataKey="createCategory"/> + </createData> + <createData entity="ApiSimpleProduct" stepKey="createProduct6"> + <requiredEntity createDataKey="createCategory"/> + </createData> + <createData entity="ApiSimpleProduct" stepKey="createProduct7"> + <requiredEntity createDataKey="createCategory"/> + </createData> + <createData entity="ApiSimpleProduct" stepKey="createProduct8"> + <requiredEntity createDataKey="createCategory"/> + </createData> + <createData entity="ApiSimpleProduct" stepKey="createProduct9"> + <requiredEntity createDataKey="createCategory"/> + </createData> + <createData entity="ApiSimpleProduct" stepKey="createProduct10"> + <requiredEntity createDataKey="createCategory"/> + </createData> + <createData entity="ApiSimpleProduct" stepKey="createProduct11"> + <requiredEntity createDataKey="createCategory"/> + </createData> + <createData entity="ApiSimpleProduct" 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="ApiSimpleProduct" stepKey="createProduct21"> + <requiredEntity createDataKey="createCategory"/> + </createData> + <createData entity="ApiSimpleProduct" stepKey="createProduct22"> + <requiredEntity createDataKey="createCategory"/> + </createData> + </before> + <after> + <!--Delete created products--> + <actionGroup ref="deleteProductUsingProductGrid" stepKey="deleteProduct"> + <argument name="product" value="GroupedProduct"/> + </actionGroup> + <deleteData createDataKey="createProduct" stepKey="deleteProduct1"/> + <deleteData createDataKey="createProduct1" stepKey="deleteProduct2"/> + <deleteData createDataKey="createProduct2" stepKey="deleteProduct3"/> + <deleteData createDataKey="createProduct3" stepKey="deleteProduct4"/> + <deleteData createDataKey="createProduct4" stepKey="deleteProduct5"/> + <deleteData createDataKey="createProduct5" stepKey="deleteProduct6"/> + <deleteData createDataKey="createProduct6" stepKey="deleteProduct7"/> + <deleteData createDataKey="createProduct7" stepKey="deleteProduct8"/> + <deleteData createDataKey="createProduct8" stepKey="deleteProduct9"/> + <deleteData createDataKey="createProduct9" stepKey="deleteProduct10"/> + <deleteData createDataKey="createProduct10" stepKey="deleteProduct11"/> + <deleteData createDataKey="createProduct11" stepKey="deleteProduct12"/> + <deleteData createDataKey="createProduct12" stepKey="deleteProduct13"/> + <deleteData createDataKey="createProduct13" stepKey="deleteProduct14"/> + <deleteData createDataKey="createProduct14" stepKey="deleteProduct15"/> + <deleteData createDataKey="createProduct15" stepKey="deleteProduct16"/> + <deleteData createDataKey="createProduct16" stepKey="deleteProduct17"/> + <deleteData createDataKey="createProduct17" stepKey="deleteProduct18"/> + <deleteData createDataKey="createProduct18" stepKey="deleteProduct19"/> + <deleteData createDataKey="createProduct19" stepKey="deleteProduct20"/> + <deleteData createDataKey="createProduct20" stepKey="deleteProduct21"/> + <deleteData createDataKey="createProduct21" stepKey="deleteProduct22"/> + <deleteData createDataKey="createProduct22" stepKey="deleteProduct23"/> + <deleteData createDataKey="createCategory" stepKey="deleteCategory"/> + <actionGroup ref="logout" stepKey="logout"/> + </after> + + <!--Create grouped Product--> + <amOnPage url="{{AdminProductIndexPage.url}}" stepKey="navigateToProductIndex"/> + <actionGroup ref="goToCreateProductPage" stepKey="goToCreateProduct"> + <argument name="product" value="GroupedProduct"/> + </actionGroup> + <actionGroup ref="fillProductNameAndSkuInProductForm" stepKey="fillProductForm"> + <argument name="product" value="GroupedProduct"/> + </actionGroup> + + <scrollTo selector="{{AdminProductFormGroupedProductsSection.toggleGroupedProduct}}" x="0" y="-100" stepKey="scrollToGroupedSection"/> + <conditionalClick selector="{{AdminProductFormGroupedProductsSection.toggleGroupedProduct}}" + dependentSelector="{{AdminProductFormGroupedProductsSection.addProductsToGroup}}" + visible="false" + stepKey="openGroupedProductsSection" + /> + + <click selector="{{AdminProductFormGroupedProductsSection.buttonContainer}}" stepKey="moveFocusToGroup"/> + <click selector="{{AdminProductFormGroupedProductsSection.addProductsToGroup}}" stepKey="clickAddProductsToGroup"/> + <waitForElementVisible selector="{{AdminAddProductsToGroupPanelSection.filters}}" stepKey="waitForGroupedProductModal"/> + <actionGroup ref="AdminGridFilterSearchResultsByInput" stepKey="filterSimplesForGroupedProduct"> + <argument name="inputName" value="sku"/> + <argument name="value" value="{{ApiSimpleProduct.sku}}"/> + </actionGroup> + + <!-- Select all, then start the bulk update attributes flow --> + <click selector="{{AdminProductGridSection.multicheckDropdown}}" stepKey="openMulticheckDropdown"/> + <click selector="{{AdminProductGridSection.multicheckOption('Select All')}}" stepKey="selectAllProductInFilteredGrid"/> + <click selector="{{AdminAddProductsToGroupPanelSection.addSelectedProducts}}" stepKey="clickAddSelectedGroupProducts"/> + + <actionGroup ref="saveProductForm" stepKey="saveProduct"/> + + <!--Open created Grouped Product--> + <amOnPage url="{{AdminProductIndexPage.url}}" stepKey="goToProductIndex"/> + <actionGroup ref="searchAdminDataGridByKeyword" stepKey="searchProductGridForm"> + <argument name="keyword" value="GroupedProduct.name"/> + </actionGroup> + <click selector="{{AdminProductGridSection.selectRowBasedOnName(GroupedProduct.name)}}" stepKey="openGroupedProduct"/> + <waitForPageLoad stepKey="waitForProductEditPageLoad"/> + + <scrollTo selector="{{AdminProductFormGroupedProductsSection.toggleGroupedProduct}}" x="0" y="-100" stepKey="scrollToGroupedSection2"/> + <conditionalClick selector="{{AdminProductFormGroupedProductsSection.toggleGroupedProduct}}" + dependentSelector="{{AdminProductFormGroupedProductsSection.addProductsToGroup}}" + visible="false" + stepKey="openGroupedProductsSection2" + /> + + <!--Change position value for the Product Position 0--> + <grabTextFrom selector="{{AdminProductFormGroupedProductsSection.nameProductFromGrid('1')}}" stepKey="grabNameProductPosition0"/> + <grabTextFrom selector="{{AdminProductFormGroupedProductsSection.nameProductFromGrid('2')}}" stepKey="grabNameProductPositionFirst"/> + <fillField selector="{{AdminProductFormGroupedProductsSection.positionProduct('1')}}" userInput="21" stepKey="fillFieldProductPosition0"/> + <click selector="{{AdminProductFormGroupedProductsSection.nextActionButton}}" stepKey="clickButton"/> + <waitForAjaxLoad stepKey="waitForAjax"/> + + <!--Go to next page and verify that Products in grid were recalculated--> + <click selector="{{AdminProductFormGroupedProductsSection.nextActionButton}}" stepKey="clickNextActionButton"/> + <waitForAjaxLoad stepKey="waitForAjax1"/> + + <grabTextFrom selector="{{AdminProductFormGroupedProductsSection.nameProductFromGrid('2')}}" stepKey="grabNameProductPosition21"/> + <assertEquals stepKey="assertProductsRecalculated"> + <actualResult type="string">$grabNameProductPosition0</actualResult> + <expectedResult type="string">$grabNameProductPosition21</expectedResult> + </assertEquals> + + <!--Change position value for the product to 1--> + <fillField selector="{{AdminProductFormGroupedProductsSection.positionProduct('2')}}" userInput="1" stepKey="fillFieldProductPosition1"/> + <click selector="{{AdminProductFormGroupedProductsSection.previousActionButton}}" stepKey="clickButton2"/> + <waitForAjaxLoad stepKey="waitForAjax2"/> + + <!--Go to previous page and verify that Products in grid were recalculated--> + <click selector="{{AdminProductFormGroupedProductsSection.previousActionButton}}" stepKey="clickPreviousActionButton"/> + <waitForAjaxLoad stepKey="waitForAjax3"/> + <grabTextFrom selector="{{AdminProductFormGroupedProductsSection.nameProductFromGrid('2')}}" stepKey="grabNameProductPosition2"/> + <grabTextFrom selector="{{AdminProductFormGroupedProductsSection.nameProductFromGrid('1')}}" stepKey="grabNameProductPositionZero"/> + <assertEquals stepKey="assertProductsRecalculated2"> + <actualResult type="string">$grabNameProductPosition2</actualResult> + <expectedResult type="string">$grabNameProductPosition0</expectedResult> + </assertEquals> + <assertEquals stepKey="assertProductsRecalculated3"> + <actualResult type="string">$grabNameProductPositionFirst</actualResult> + <expectedResult type="string">$grabNameProductPositionZero</expectedResult> + </assertEquals> + </test> +</tests> diff --git a/app/code/Magento/GroupedProduct/Test/Unit/Ui/DataProvider/Product/Form/Modifier/GroupedTest.php b/app/code/Magento/GroupedProduct/Test/Unit/Ui/DataProvider/Product/Form/Modifier/GroupedTest.php index 6ef87117deae2..327b47d4a75d8 100644 --- a/app/code/Magento/GroupedProduct/Test/Unit/Ui/DataProvider/Product/Form/Modifier/GroupedTest.php +++ b/app/code/Magento/GroupedProduct/Test/Unit/Ui/DataProvider/Product/Form/Modifier/GroupedTest.php @@ -34,6 +34,7 @@ class GroupedTest extends AbstractModifierTest const LINKED_PRODUCT_NAME = 'linked'; const LINKED_PRODUCT_QTY = '0'; const LINKED_PRODUCT_POSITION = 1; + const LINKED_PRODUCT_POSITION_CALCULATED = 1; const LINKED_PRODUCT_PRICE = '1'; /** @@ -212,6 +213,7 @@ public function testModifyData() 'price' => null, 'qty' => self::LINKED_PRODUCT_QTY, 'position' => self::LINKED_PRODUCT_POSITION, + 'positionCalculated' => self::LINKED_PRODUCT_POSITION_CALCULATED, 'thumbnail' => null, 'type_id' => null, 'status' => null, diff --git a/app/code/Magento/GroupedProduct/Ui/DataProvider/Product/Form/Modifier/Grouped.php b/app/code/Magento/GroupedProduct/Ui/DataProvider/Product/Form/Modifier/Grouped.php index 2d1a1d19757e2..57d9bc78aaf28 100644 --- a/app/code/Magento/GroupedProduct/Ui/DataProvider/Product/Form/Modifier/Grouped.php +++ b/app/code/Magento/GroupedProduct/Ui/DataProvider/Product/Form/Modifier/Grouped.php @@ -133,7 +133,7 @@ public function __construct( } /** - * {@inheritdoc} + * @inheritdoc */ public function modifyData(array $data) { @@ -143,12 +143,17 @@ public function modifyData(array $data) if ($modelId) { $storeId = $this->locator->getStore()->getId(); $data[$product->getId()]['links'][self::LINK_TYPE] = []; - foreach ($this->productLinkRepository->getList($product) as $linkItem) { + $linkedItems = $this->productLinkRepository->getList($product); + usort($linkedItems, function ($a, $b) { + return $a->getPosition() <=> $b->getPosition(); + }); + foreach ($linkedItems as $index => $linkItem) { if ($linkItem->getLinkType() !== self::LINK_TYPE) { continue; } /** @var \Magento\Catalog\Api\Data\ProductInterface $linkedProduct */ $linkedProduct = $this->productRepository->get($linkItem->getLinkedProductSku(), false, $storeId); + $linkItem->setPosition($index); $data[$modelId]['links'][self::LINK_TYPE][] = $this->fillData($linkedProduct, $linkItem); } $data[$modelId][self::DATA_SOURCE_DEFAULT]['current_store_id'] = $storeId; @@ -175,6 +180,7 @@ protected function fillData(ProductInterface $linkedProduct, ProductLinkInterfac 'price' => $currency->toCurrency(sprintf("%f", $linkedProduct->getPrice())), 'qty' => $linkItem->getExtensionAttributes()->getQty(), 'position' => $linkItem->getPosition(), + 'positionCalculated' => $linkItem->getPosition(), 'thumbnail' => $this->imageHelper->init($linkedProduct, 'product_listing_thumbnail')->getUrl(), 'type_id' => $linkedProduct->getTypeId(), 'status' => $this->status->getOptionText($linkedProduct->getStatus()), @@ -185,7 +191,7 @@ protected function fillData(ProductInterface $linkedProduct, ProductLinkInterfac } /** - * {@inheritdoc} + * @inheritdoc */ public function modifyMeta(array $meta) { @@ -454,7 +460,7 @@ protected function getGrid() 'label' => null, 'renderDefaultRecord' => false, 'template' => 'ui/dynamic-rows/templates/grid', - 'component' => 'Magento_Ui/js/dynamic-rows/dynamic-rows-grid', + 'component' => 'Magento_GroupedProduct/js/grouped-product-grid', 'addButton' => false, 'itemTemplate' => 'record', 'dataScope' => 'data.links', @@ -555,6 +561,22 @@ protected function fillMeta() ], ], ], + 'positionCalculated' => [ + 'arguments' => [ + 'data' => [ + 'config' => [ + 'label' => __('Position'), + 'dataType' => Form\Element\DataType\Number::NAME, + 'formElement' => Form\Element\Input::NAME, + 'componentType' => Form\Field::NAME, + 'elementTmpl' => 'Magento_GroupedProduct/components/position', + 'sortOrder' => 90, + 'fit' => true, + 'dataScope' => 'positionCalculated' + ], + ], + ], + ], 'actionDelete' => [ 'arguments' => [ 'data' => [ @@ -563,7 +585,7 @@ protected function fillMeta() 'componentType' => 'actionDelete', 'dataType' => Form\Element\DataType\Text::NAME, 'label' => __('Actions'), - 'sortOrder' => 90, + 'sortOrder' => 100, 'fit' => true, ], ], @@ -577,7 +599,7 @@ protected function fillMeta() 'formElement' => Form\Element\Input::NAME, 'componentType' => Form\Field::NAME, 'dataScope' => 'position', - 'sortOrder' => 100, + 'sortOrder' => 110, 'visible' => false, ], ], diff --git a/app/code/Magento/GroupedProduct/view/adminhtml/web/css/grouped-product.css b/app/code/Magento/GroupedProduct/view/adminhtml/web/css/grouped-product.css index 3d723387d23b0..9142eaf90899e 100644 --- a/app/code/Magento/GroupedProduct/view/adminhtml/web/css/grouped-product.css +++ b/app/code/Magento/GroupedProduct/view/adminhtml/web/css/grouped-product.css @@ -66,3 +66,49 @@ overflow: hidden; text-overflow: ellipsis; } + +.position { + width: 100px; +} + +.icon-rearrange-position > span { + border: 0; + clip: rect(0, 0, 0, 0); + height: 1px; + margin: -1px; + overflow: hidden; + padding: 0; + position: absolute; + width: 1px; +} + +.icon-forward, +.icon-backward { + -webkit-font-smoothing: antialiased; + font-family: 'Admin Icons'; + font-size: 17px; + speak: none; +} + +.position > * { + float: left; + margin: 3px; +} + +.position-widget-input { + text-align: center; + width: 40px; +} + +.icon-forward:before { + content: '\e618'; +} + +.icon-backward:before { + content: '\e619'; +} + +.icon-rearrange-position, .icon-rearrange-position:hover { + color: #d9d9d9; + text-decoration: none; +} diff --git a/app/code/Magento/GroupedProduct/view/adminhtml/web/js/grouped-product-grid.js b/app/code/Magento/GroupedProduct/view/adminhtml/web/js/grouped-product-grid.js new file mode 100644 index 0000000000000..0ac3b58d6e3a7 --- /dev/null +++ b/app/code/Magento/GroupedProduct/view/adminhtml/web/js/grouped-product-grid.js @@ -0,0 +1,219 @@ +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +define([ + 'underscore', + 'uiRegistry', + 'Magento_Ui/js/dynamic-rows/dynamic-rows-grid' +], function (_, registry, dynamicRowsGrid) { + 'use strict'; + + return dynamicRowsGrid.extend({ + + /** + * Set max element position + * + * @param {Number} position - element position + * @param {Object} elem - instance + */ + setMaxPosition: function (position, elem) { + + if (position || position === 0) { + this.checkMaxPosition(position); + this.sort(position, elem); + + if (~~position === this.maxPosition && ~~position > this.getDefaultPageBoundary() + 1) { + this.shiftNextPagesPositions(position); + } + } else { + this.maxPosition += 1; + } + }, + + /** + * Shift positions for next page elements + * + * @param {Number} position + */ + shiftNextPagesPositions: function (position) { + + var recordData = this.recordData(), + startIndex = ~~this.currentPage() * this.pageSize, + offset = position - startIndex + 1, + index = startIndex; + + if (~~this.currentPage() === this.pages()) { + return false; + } + + for (index; index < recordData.length; index++) { + recordData[index].position = index + offset; + } + this.recordData(recordData); + }, + + /** + * Update position for element after position from another page is entered + * + * @param {Object} data + * @param {Object} event + */ + updateGridPosition: function (data, event) { + var inputValue = parseInt(event.target.value, 10), + recordData = this.recordData(), + record, + previousValue, + updatedRecord; + + record = this.elems().find(function (obj) { + return obj.dataScope === data.parentScope; + }); + + previousValue = this.getCalculatedPosition(record); + + if (isNaN(inputValue) || inputValue < 0 || inputValue === previousValue) { + return false; + } + + this.elems([]); + + updatedRecord = this.getUpdatedRecordIndex(recordData, record.data().id); + + if (inputValue >= this.recordData().size() - 1) { + recordData[updatedRecord].position = this.getGlobalMaxPosition() + 1; + } else { + recordData.forEach(function (value, index) { + if (~~value.id === ~~record.data().id) { + recordData[index].position = inputValue; + } else if (inputValue > previousValue && index <= inputValue) { + recordData[index].position = index - 1; + } else if (inputValue < previousValue && index >= inputValue) { + recordData[index].position = index + 1; + } + }); + } + + this.reloadGridData(recordData); + + }, + + /** + * Get updated record index + * + * @param {Array} recordData + * @param {Number} recordId + * @return {Number} + */ + getUpdatedRecordIndex: function (recordData, recordId) { + return recordData.map(function (o) { + return ~~o.id; + }).indexOf(~~recordId); + }, + + /** + * + * @param {Array} recordData - to reprocess + */ + reloadGridData: function (recordData) { + this.recordData(recordData.sort(function (a, b) { + return ~~a.position - ~~b.position; + })); + this._updateCollection(); + this.reload(); + }, + + /** + * Event handler for "Send to bottom" button + * + * @param {Object} positionObj + * @return {Boolean} + */ + sendToBottom: function (positionObj) { + + var objectToUpdate = this.getObjectToUpdate(positionObj), + recordData = this.recordData(), + updatedRecord; + + if (~~this.currentPage() === this.pages) { + objectToUpdate.position = this.maxPosition; + } else { + this.elems([]); + updatedRecord = this.getUpdatedRecordIndex(recordData, objectToUpdate.data().id); + recordData[updatedRecord].position = this.getGlobalMaxPosition() + 1; + this.reloadGridData(recordData); + } + + return false; + }, + + /** + * Event handler for "Send to top" button + * + * @param {Object} positionObj + * @return {Boolean} + */ + sendToTop: function (positionObj) { + var objectToUpdate = this.getObjectToUpdate(positionObj), + recordData = this.recordData(), + updatedRecord; + + //isFirst + if (~~this.currentPage() === 1) { + objectToUpdate.position = 0; + } else { + this.elems([]); + updatedRecord = this.getUpdatedRecordIndex(recordData, objectToUpdate.data().id); + recordData.forEach(function (value, index) { + recordData[index].position = index === updatedRecord ? 0 : value.position + 1; + }); + this.reloadGridData(recordData); + } + + return false; + }, + + /** + * Get element from grid for update + * + * @param {Object} object + * @return {*} + */ + getObjectToUpdate: function (object) { + return this.elems().filter(function (item) { + return item.name === object.parentName; + })[0]; + }, + + /** + * Value function for position input + * + * @param {Object} data + * @return {Number} + */ + getCalculatedPosition: function (data) { + return (~~this.currentPage() - 1) * this.pageSize + this.elems().pluck('name').indexOf(data.name); + }, + + /** + * Return Page Boundary + * + * @return {Number} + */ + getDefaultPageBoundary: function () { + return ~~this.currentPage() * this.pageSize - 1; + }, + + /** + * Returns position for last element to be moved after + * + * @return {Number} + */ + getGlobalMaxPosition: function () { + return _.max(this.recordData().map(function (r) { + return ~~r.position; + })); + } + }); +}); diff --git a/app/code/Magento/GroupedProduct/view/adminhtml/web/template/components/position.html b/app/code/Magento/GroupedProduct/view/adminhtml/web/template/components/position.html new file mode 100644 index 0000000000000..9d1e464e58b62 --- /dev/null +++ b/app/code/Magento/GroupedProduct/view/adminhtml/web/template/components/position.html @@ -0,0 +1,15 @@ +<div class="position"> + <a href="#" class="move-top icon-backward icon-rearrange-position" + data-bind="click: $parent.sendToTop.bind($parent)"> + <span>Top</span> + </a> + <input type="text" class="position-widget-input" + data-bind=" + value: $parent.getCalculatedPosition($record()), + event: {blur: $parent.updateGridPosition.bind($parent)} + "/> + <a href="#" class="move-bottom icon-forward icon-rearrange-position" + data-bind="click: $parent.sendToBottom.bind($parent)"> + <span>Bottom</span> + </a> +</div> diff --git a/app/code/Magento/ImportExport/Controller/Adminhtml/Import/Download.php b/app/code/Magento/ImportExport/Controller/Adminhtml/Import/Download.php index 33dbba8320051..8b16e232856cd 100644 --- a/app/code/Magento/ImportExport/Controller/Adminhtml/Import/Download.php +++ b/app/code/Magento/ImportExport/Controller/Adminhtml/Import/Download.php @@ -5,9 +5,9 @@ */ namespace Magento\ImportExport\Controller\Adminhtml\Import; +use Magento\Framework\App\Filesystem\DirectoryList; use Magento\Framework\Component\ComponentRegistrar; use Magento\ImportExport\Controller\Adminhtml\Import as ImportController; -use Magento\Framework\App\Filesystem\DirectoryList; /** * Download sample file controller @@ -36,6 +36,11 @@ class Download extends ImportController */ protected $fileFactory; + /** + * @var \Magento\ImportExport\Model\Import\SampleFileProvider + */ + private $sampleFileProvider; + /** * Constructor * @@ -44,46 +49,48 @@ class Download extends ImportController * @param \Magento\Framework\Controller\Result\RawFactory $resultRawFactory * @param \Magento\Framework\Filesystem\Directory\ReadFactory $readFactory * @param ComponentRegistrar $componentRegistrar + * @param \Magento\ImportExport\Model\Import\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\Framework\Component\ComponentRegistrar $componentRegistrar, + \Magento\ImportExport\Model\Import\SampleFileProvider $sampleFileProvider = null ) { - parent::__construct( - $context - ); + parent::__construct($context); $this->fileFactory = $fileFactory; $this->resultRawFactory = $resultRawFactory; $this->readFactory = $readFactory; $this->componentRegistrar = $componentRegistrar; + $this->sampleFileProvider = $sampleFileProvider + ?: \Magento\Framework\App\ObjectManager::getInstance() + ->get(\Magento\ImportExport\Model\Import\SampleFileProvider::class); } /** * Download sample file action * - * @return \Magento\Framework\Controller\Result\Raw + * @return \Magento\Framework\Controller\Result\Raw|\Magento\Framework\Controller\Result\Redirect */ public function execute() { - $fileName = $this->getRequest()->getParam('filename') . '.csv'; - $moduleDir = $this->componentRegistrar->getPath(ComponentRegistrar::MODULE, self::SAMPLE_FILES_MODULE); - $fileAbsolutePath = $moduleDir . '/Files/Sample/' . $fileName; - $directoryRead = $this->readFactory->create($moduleDir); - $filePath = $directoryRead->getRelativePath($fileAbsolutePath); + $entityName = $this->getRequest()->getParam('filename'); - if (!$directoryRead->isFile($filePath)) { + try { + $fileContents = $this->sampleFileProvider->getFileContents($entityName); + } catch (\Magento\Framework\Exception\NoSuchEntityException $e) { /** @var \Magento\Backend\Model\View\Result\Redirect $resultRedirect */ $this->messageManager->addError(__('There is no sample file for this entity.')); $resultRedirect = $this->resultRedirectFactory->create(); $resultRedirect->setPath('*/import'); + return $resultRedirect; } - $fileSize = isset($directoryRead->stat($filePath)['size']) - ? $directoryRead->stat($filePath)['size'] : null; + $fileSize = $this->sampleFileProvider->getSize($entityName); + $fileName = $entityName . '.csv'; $this->fileFactory->create( $fileName, @@ -95,7 +102,8 @@ public function execute() /** @var \Magento\Framework\Controller\Result\Raw $resultRaw */ $resultRaw = $this->resultRawFactory->create(); - $resultRaw->setContents($directoryRead->readFile($filePath)); + $resultRaw->setContents($fileContents); + return $resultRaw; } } diff --git a/app/code/Magento/ImportExport/Controller/Adminhtml/Import/Start.php b/app/code/Magento/ImportExport/Controller/Adminhtml/Import/Start.php index 695a0e61709f1..42d3ef696b8bc 100644 --- a/app/code/Magento/ImportExport/Controller/Adminhtml/Import/Start.php +++ b/app/code/Magento/ImportExport/Controller/Adminhtml/Import/Start.php @@ -7,7 +7,11 @@ use Magento\ImportExport\Controller\Adminhtml\ImportResult as ImportResultController; use Magento\Framework\Controller\ResultFactory; +use Magento\ImportExport\Model\Import; +/** + * Controller responsible for initiating the import process. + */ class Start extends ImportResultController { /** @@ -62,6 +66,11 @@ public function execute() $this->importModel->setData($data); $errorAggregator = $this->importModel->getErrorAggregator(); + $errorAggregator->initValidationStrategy( + $this->importModel->getData(Import::FIELD_NAME_VALIDATION_STRATEGY), + $this->importModel->getData(Import::FIELD_NAME_ALLOWED_ERROR_COUNT) + ); + try { $this->importModel->importSource(); } catch (\Exception $e) { diff --git a/app/code/Magento/ImportExport/Model/Import.php b/app/code/Magento/ImportExport/Model/Import.php index 85efb85a6a7c8..6ff2ca8c4d0eb 100644 --- a/app/code/Magento/ImportExport/Model/Import.php +++ b/app/code/Magento/ImportExport/Model/Import.php @@ -5,11 +5,12 @@ */ // @codingStandardsIgnoreFile - namespace Magento\ImportExport\Model; +use Magento\Framework\App\ObjectManager; use Magento\Framework\App\Filesystem\DirectoryList; use Magento\Framework\HTTP\Adapter\FileTransferFactory; +use Magento\Framework\Message\ManagerInterface; use Magento\Framework\Stdlib\DateTime\DateTime; use Magento\ImportExport\Model\Import\ErrorProcessing\ProcessingError; use Magento\ImportExport\Model\Import\ErrorProcessing\ProcessingErrorAggregatorInterface; @@ -22,6 +23,7 @@ * @method string getBehavior() getBehavior() * @method \Magento\ImportExport\Model\Import setEntity() setEntity(string $value) * @SuppressWarnings(PHPMD.CouplingBetweenObjects) + * @SuppressWarnings(PHPMD.ExcessiveClassComplexity) * @since 100.0.2 */ class Import extends \Magento\ImportExport\Model\AbstractModel @@ -180,6 +182,11 @@ class Import extends \Magento\ImportExport\Model\AbstractModel */ private $localeDate; + /** + * @var ManagerInterface + */ + private $messageManager; + /** * @param \Psr\Log\LoggerInterface $logger * @param \Magento\Framework\Filesystem $filesystem @@ -196,6 +203,7 @@ class Import extends \Magento\ImportExport\Model\AbstractModel * @param History $importHistoryModel * @param DateTime $localeDate * @param array $data + * @param ManagerInterface|null $messageManager * @SuppressWarnings(PHPMD.ExcessiveParameterList) */ public function __construct( @@ -213,7 +221,8 @@ public function __construct( \Magento\Framework\Indexer\IndexerRegistry $indexerRegistry, \Magento\ImportExport\Model\History $importHistoryModel, DateTime $localeDate, - array $data = [] + array $data = [], + ManagerInterface $messageManager = null ) { $this->_importExportData = $importExportData; $this->_coreConfig = $coreConfig; @@ -228,6 +237,7 @@ public function __construct( $this->_filesystem = $filesystem; $this->importHistoryModel = $importHistoryModel; $this->localeDate = $localeDate; + $this->messageManager = $messageManager ?: ObjectManager::getInstance()->get(ManagerInterface::class); parent::__construct($logger, $filesystem, $data); } @@ -456,6 +466,8 @@ public function importSource() } /** + * Process import. + * * @return bool * @throws \Magento\Framework\Exception\LocalizedException */ @@ -475,6 +487,8 @@ public function isImportAllowed() } /** + * Get error aggregator instance. + * * @return ProcessingErrorAggregatorInterface * @throws \Magento\Framework\Exception\LocalizedException */ @@ -484,7 +498,7 @@ public function getErrorAggregator() } /** - * Move uploaded file and create source adapter instance. + * Move uploaded file. * * @throws \Magento\Framework\Exception\LocalizedException * @return string Source file path @@ -610,10 +624,20 @@ public function validateSource(\Magento\ImportExport\Model\Import\AbstractSource $messages = $this->getOperationResultMessages($errorAggregator); $this->addLogComment($messages); - $result = !$errorAggregator->getErrorsCount(); + $errorsCount = $errorAggregator->getErrorsCount(); + $result = !$errorsCount; + $validationStrategy = $this->getData(self::FIELD_NAME_VALIDATION_STRATEGY); + if ($errorsCount + && $validationStrategy === ProcessingErrorAggregatorInterface::VALIDATION_STRATEGY_SKIP_ERRORS + ) { + $this->messageManager->addWarningMessage(__('Skipped errors: %1', $errorsCount)); + $result = true; + } + if ($result) { $this->addLogComment(__('Import data validation is complete.')); } + return $result; } diff --git a/app/code/Magento/ImportExport/Model/Import/SampleFileProvider.php b/app/code/Magento/ImportExport/Model/Import/SampleFileProvider.php new file mode 100644 index 0000000000000..badffa6632174 --- /dev/null +++ b/app/code/Magento/ImportExport/Model/Import/SampleFileProvider.php @@ -0,0 +1,127 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\ImportExport\Model\Import; + +use Magento\Framework\Component\ComponentRegistrar; +use Magento\Framework\Exception\NoSuchEntityException; +use Magento\Framework\Filesystem\Directory\ReadInterface; +use Magento\Framework\Filesystem\Directory\ReadFactory; + +/** + * Import sample file provider model. + * + * This class support only *.csv. + */ +class SampleFileProvider +{ + /** + * Associate an import entity to its module, e.g ['entity_name' => 'module_name'] + * + * @var array + */ + private $samples; + + /** + * @var ComponentRegistrar + */ + private $componentRegistrar; + + /** + * @var ReadFactory + */ + private $readFactory; + + /** + * @param ReadFactory $readFactory + * @param ComponentRegistrar $componentRegistrar + * @param array $samples + */ + public function __construct( + ReadFactory $readFactory, + ComponentRegistrar $componentRegistrar, + array $samples = [] + ) { + $this->readFactory = $readFactory; + $this->componentRegistrar = $componentRegistrar; + $this->samples = $samples; + } + + /** + * Returns the size for the given file associated to an import entity. + * + * @param string $entityName + * @return int|null + */ + public function getSize(string $entityName) + { + $directoryRead = $this->getDirectoryRead($entityName); + $filePath = $this->getPath($entityName); + $fileSize = $directoryRead->stat($filePath)['size'] ?? null; + + return $fileSize; + } + + /** + * Returns content for the given file associated to an import entity. + * + * @param string $entityName + * @return string + */ + public function getFileContents(string $entityName): string + { + $directoryRead = $this->getDirectoryRead($entityName); + $filePath = $this->getPath($entityName); + + return $directoryRead->readFile($filePath); + } + + /** + * @param string $entityName + * @return string + * @throws NoSuchEntityException + */ + private function getPath(string $entityName): string + { + $directoryRead = $this->getDirectoryRead($entityName); + $fileAbsolutePath = 'Files/Sample/' . $entityName . '.csv'; + $filePath = $directoryRead->getRelativePath($fileAbsolutePath); + + if (!$directoryRead->isFile($filePath)) { + throw new NoSuchEntityException(__("There is no file: %file", ['file' => $filePath])); + } + + return $filePath; + } + + /** + * @param string $entityName + * @return ReadInterface + */ + private function getDirectoryRead(string $entityName): ReadInterface + { + $moduleName = $this->getModuleName($entityName); + $moduleDir = $this->componentRegistrar->getPath(ComponentRegistrar::MODULE, $moduleName); + $directoryRead = $this->readFactory->create($moduleDir); + + return $directoryRead; + } + + /** + * @param string $entityName + * @return string + * @throws NoSuchEntityException + */ + private function getModuleName(string $entityName): string + { + if (!isset($this->samples[$entityName])) { + throw new NoSuchEntityException(); + } + + return $this->samples[$entityName]; + } +} diff --git a/app/code/Magento/ImportExport/Test/Mftf/Page/AdminImportIndexPage.xml b/app/code/Magento/ImportExport/Test/Mftf/Page/AdminImportIndexPage.xml new file mode 100644 index 0000000000000..87807eb9b0e85 --- /dev/null +++ b/app/code/Magento/ImportExport/Test/Mftf/Page/AdminImportIndexPage.xml @@ -0,0 +1,15 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<pages xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Page/etc/PageObject.xsd"> + <page name="AdminImportIndexPage" url="admin/import/" area="admin" module="Magento_ImportExport"> + <section name="AdminImportHeaderSection"/> + <section name="AdminImportMainSection"/> + </page> +</pages> diff --git a/app/code/Magento/ImportExport/Test/Mftf/Section/AdminImportHeaderSection.xml b/app/code/Magento/ImportExport/Test/Mftf/Section/AdminImportHeaderSection.xml new file mode 100644 index 0000000000000..748580be09406 --- /dev/null +++ b/app/code/Magento/ImportExport/Test/Mftf/Section/AdminImportHeaderSection.xml @@ -0,0 +1,14 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<sections xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Page/etc/SectionObject.xsd"> + <section name="AdminImportHeaderSection"> + <element name="checkDataButton" type="button" selector="#upload_button" timeout="30"/> + </section> +</sections> diff --git a/app/code/Magento/ImportExport/Test/Mftf/Section/AdminImportMainSection.xml b/app/code/Magento/ImportExport/Test/Mftf/Section/AdminImportMainSection.xml new file mode 100644 index 0000000000000..2ce6b1e35777f --- /dev/null +++ b/app/code/Magento/ImportExport/Test/Mftf/Section/AdminImportMainSection.xml @@ -0,0 +1,17 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<sections xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Page/etc/SectionObject.xsd"> + <section name="AdminImportMainSection"> + <element name="entityType" type="select" selector="#entity"/> + <element name="importBehavior" type="select" selector="#basic_behavior"/> + <element name="selectFileToImport" type="input" selector="#import_file"/> + <element name="importButton" type="button" selector="#import_validation_container button" timeout="30"/> + </section> +</sections> diff --git a/app/code/Magento/ImportExport/Test/Mftf/Test/AdminImportProductsWithDeleteBehaviorTest.xml b/app/code/Magento/ImportExport/Test/Mftf/Test/AdminImportProductsWithDeleteBehaviorTest.xml new file mode 100644 index 0000000000000..04cef6d6db232 --- /dev/null +++ b/app/code/Magento/ImportExport/Test/Mftf/Test/AdminImportProductsWithDeleteBehaviorTest.xml @@ -0,0 +1,65 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> + <test name="AdminImportProductsWithDeleteBehaviorTest"> + <annotations> + <description value="Verify Magento native import products with delete behavior."/> + <stories value="Verify Magento native import products with delete behavior."/> + <features value="Import/Export"/> + <title value="Verify Magento native import products with delete behavior."/> + <severity value="CRITICAL"/> + <testCaseId value="MAGETWO-79495"/> + <group value="importExport"/> + </annotations> + <before> + <!--Create Simple product--> + <createData entity="SimpleProduct3" stepKey="createSimpleProduct"> + <field key="name">Simple Product for Test</field> + <field key="sku">Simple Product for Test</field> + </createData> + <!--Create Virtual product--> + <createData entity="VirtualProduct" stepKey="createVirtualProduct"> + <field key="name">Virtual Product for Test</field> + <field key="sku">Virtual Product for Test</field> + </createData> + <!-- Create Downloadable product --> + <createData entity="ApiDownloadableProduct" stepKey="createDownloadableProduct"> + <field key="name">Api Downloadable Product for Test</field> + <field key="sku">Api Downloadable Product for Test</field> + </createData> + <actionGroup ref="LoginAsAdmin" stepKey="loginAsAdmin"/> + </before> + <after> + <amOnPage url="{{AdminProductIndexPage.url}}" stepKey="navigateToProductIndexPage"/> + <actionGroup ref="clearFiltersAdminDataGrid" stepKey="clearProductFilters"/> + <actionGroup ref="logout" stepKey="logoutFromAdmin"/> + </after> + <amOnPage url="{{AdminImportIndexPage.url}}" stepKey="goToImportIndexPage"/> + <selectOption selector="{{AdminImportMainSection.entityType}}" userInput="Products" stepKey="selectProductsOption"/> + <waitForElementVisible selector="{{AdminImportMainSection.importBehavior}}" stepKey="waitForImportBehaviorElementVisible"/> + <selectOption selector="{{AdminImportMainSection.importBehavior}}" userInput="Delete" stepKey="selectDeleteOption"/> + <attachFile selector="{{AdminImportMainSection.selectFileToImport}}" userInput="catalog_products.csv" stepKey="attachFileForImport"/> + <click selector="{{AdminImportHeaderSection.checkDataButton}}" stepKey="clickCheckDataButton"/> + <click selector="{{AdminImportMainSection.importButton}}" stepKey="clickImportButton"/> + <see selector="{{AdminMessagesSection.successMessage}}" userInput="Import successfully done" stepKey="assertSuccessMessage"/> + <actionGroup ref="SearchForProductOnBackendActionGroup" stepKey="searchSimpleProductOnBackend"> + <argument name="product" value="$$createSimpleProduct$$"/> + </actionGroup> + <see selector="{{AdminDataGridTableSection.dataGridEmpty}}" userInput="We couldn't find any records." stepKey="assertDataGridEmptyMessage"/> + <actionGroup ref="SearchForProductOnBackendActionGroup" stepKey="searchVirtualProductOnBackend"> + <argument name="product" value="$$createVirtualProduct$$"/> + </actionGroup> + <see selector="{{AdminDataGridTableSection.dataGridEmpty}}" userInput="We couldn't find any records." stepKey="assertDataGridEmptyMessage1"/> + <actionGroup ref="SearchForProductOnBackendActionGroup" stepKey="searchDownloadableProductOnBackend"> + <argument name="product" value="$$createDownloadableProduct$$"/> + </actionGroup> + <see selector="{{AdminDataGridTableSection.dataGridEmpty}}" userInput="We couldn't find any records." stepKey="assertDataGridEmptyMessage2"/> + </test> +</tests> diff --git a/app/code/Magento/ImportExport/Test/Unit/Model/Import/SampleFileProviderTest.php b/app/code/Magento/ImportExport/Test/Unit/Model/Import/SampleFileProviderTest.php new file mode 100644 index 0000000000000..28ed10ac3c3b9 --- /dev/null +++ b/app/code/Magento/ImportExport/Test/Unit/Model/Import/SampleFileProviderTest.php @@ -0,0 +1,155 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +namespace Magento\ImportExport\Test\Unit\Model\Import; + +use Magento\Framework\Filesystem\Directory\ReadFactory; +use Magento\Framework\Filesystem\Directory\ReadInterface; +use Magento\Framework\TestFramework\Unit\Helper\ObjectManager; +use Magento\ImportExport\Model\Import\SampleFileProvider; + +/** + * Test class for Magento\ImportExport\Model\Import\SampleFileProvider. + */ +class SampleFileProviderTest extends \PHPUnit\Framework\TestCase +{ + /** + * @var ReadInterface|\PHPUnit_Framework_MockObject_MockObject + */ + private $readerMock; + + /** + * @var ReadFactory|\PHPUnit_Framework_MockObject_MockObject + */ + private $readerFactoryMock; + + /** + * @var ObjectManager + */ + private $objectManager; + + /** + * @var SampleFileProvider + */ + private $model; + + /** + * @var string + */ + private $entityName = 'test_sample'; + + /** + * @var string + */ + private $moduleName = 'Test_Sample'; + + /** + * @var string + */ + private $filePath = 'Files/Sample/test_sample.csv'; + + /** + * @inheritdoc + */ + protected function setUp() + { + $this->objectManager = new ObjectManager($this); + $this->readerMock = $this->getMockBuilder(ReadInterface::class) + ->disableOriginalConstructor() + ->getMockForAbstractClass(); + $this->readerFactoryMock = $this->createMock(ReadFactory::class); + $this->readerFactoryMock->expects($this->any())->method('create')->willReturn($this->readerMock); + $this->readerMock->expects($this->any())->method('getRelativePath')->willReturnArgument(0); + $this->model = $this->objectManager->getObject( + SampleFileProvider::class, + [ + 'readFactory' => $this->readerFactoryMock, + ] + ); + } + + /** + * @return void + */ + public function testGetSize() + { + $fileSize = 10; + + $this->objectManager->setBackwardCompatibleProperty( + $this->model, + 'samples', + [$this->entityName => $this->moduleName] + ); + $this->readerMock->expects($this->atLeastOnce())->method('isFile')->willReturn(true); + $this->readerMock->expects($this->once())->method('stat') + ->with($this->filePath) + ->willReturn(['size' => $fileSize]); + + $actualSize = $this->model->getSize($this->entityName); + $this->assertEquals($fileSize, $actualSize); + } + + /** + * @return void + */ + public function testGetFileContents() + { + $fileContent = 'test'; + + $this->objectManager->setBackwardCompatibleProperty( + $this->model, + 'samples', + [$this->entityName => $this->moduleName] + ); + $this->readerMock->expects($this->atLeastOnce())->method('isFile')->willReturn(true); + $this->readerMock->expects($this->once())->method('readFile') + ->with($this->filePath) + ->willReturn($fileContent); + + $actualContent = $this->model->getFileContents($this->entityName); + $this->assertEquals($fileContent, $actualContent); + } + + /** + * @dataProvider methodDataProvider + * @expectedException \Magento\Framework\Exception\NoSuchEntityException + * @param string $methodName + * @return void + */ + public function testMethodCallMissingSample(string $methodName) + { + $this->model->{$methodName}('missingType'); + } + + /** + * @dataProvider methodDataProvider + * @expectedException \Magento\Framework\Exception\NoSuchEntityException + * @expectedExceptionMessage There is no file: Files/Sample/test_sample.csv + * @param string $methodName + * @return void + */ + public function testMethodCallMissingFile(string $methodName) + { + $this->objectManager->setBackwardCompatibleProperty( + $this->model, + 'samples', + [$this->entityName => $this->moduleName] + ); + $this->readerMock->expects($this->atLeastOnce())->method('isFile')->willReturn(false); + + $this->model->{$methodName}($this->entityName); + } + + /** + * @return array + */ + public function methodDataProvider(): array + { + return [ + ['getSize'], + ['getFileContents'], + ]; + } +} diff --git a/app/code/Magento/ImportExport/etc/di.xml b/app/code/Magento/ImportExport/etc/di.xml index 47acf7a356d93..36c76022a41c7 100644 --- a/app/code/Magento/ImportExport/etc/di.xml +++ b/app/code/Magento/ImportExport/etc/di.xml @@ -17,4 +17,16 @@ </argument> </arguments> </type> + <type name="Magento\ImportExport\Model\Import\SampleFileProvider"> + <arguments> + <argument name="samples" xsi:type="array"> + <item name="advanced_pricing" xsi:type="string">Magento_ImportExport</item> + <item name="catalog_product" xsi:type="string">Magento_ImportExport</item> + <item name="customer" xsi:type="string">Magento_ImportExport</item> + <item name="customer_address" xsi:type="string">Magento_ImportExport</item> + <item name="customer_composite" xsi:type="string">Magento_ImportExport</item> + <item name="customer_finance" xsi:type="string">Magento_ImportExport</item> + </argument> + </arguments> + </type> </config> diff --git a/app/code/Magento/LayeredNavigation/Test/Mftf/Test/StorefrontCheckingResultsOfFiltersTest.xml b/app/code/Magento/LayeredNavigation/Test/Mftf/Test/StorefrontCheckingResultsOfFiltersTest.xml index 1d4a861ffe7b8..e6c3cb1788b31 100644 --- a/app/code/Magento/LayeredNavigation/Test/Mftf/Test/StorefrontCheckingResultsOfFiltersTest.xml +++ b/app/code/Magento/LayeredNavigation/Test/Mftf/Test/StorefrontCheckingResultsOfFiltersTest.xml @@ -37,7 +37,7 @@ <click selector="{{AdminProductAttributeSetGridSection.deleteOptionByName('Black')}}" stepKey="deleteOption5"/> <!--Save attribute--> - <click selector="{{AdminMainActionsSection.save}}" stepKey="clickSaveColorAttribute"/> + <actionGroup ref="SaveProductAttribute" stepKey="clickSaveColorAttribute"/> <!--Delete Category--> <deleteData createDataKey="createCategory" stepKey="deleteCategory"/> @@ -87,7 +87,7 @@ <waitForElementVisible selector="{{AdminNewAttributePanelSection.isDefault('5')}}" time="30" stepKey="waitForOptionRow5"/> <fillField selector="{{AdminNewAttributePanelSection.optionAdminValue('4')}}" userInput="{{ColorProductAttribute5.name}}" stepKey="fillAdminLabel5"/> <!--Save attribute--> - <click selector="{{AdminMainActionsSection.save}}" stepKey="clickSaveAttribute"/> + <actionGroup ref="SaveProductAttribute" stepKey="clickSaveAttribute"/> <!--Create 'material' attribute:--> <amOnPage url="{{ProductAttributePage.url}}" stepKey="goToNewProductAttributePage"/> @@ -122,9 +122,7 @@ <selectOption selector="{{AttributePropertiesSection.useInLayeredNavigation}}" userInput="1" stepKey="selectUseInLayeredNavigation"/> <!--Save attribute--> - <click selector="{{AdminNewAttributePanel.saveAttribute}}" stepKey="clickSaveNewAttribute"/> - <waitForPageLoad stepKey="waitForSavingNewAttribute"/> - <see userInput="You saved the product attribute." stepKey="seeSuccessMessage"/> + <actionGroup ref="SaveProductAttribute" stepKey="clickSaveNewAttribute"/> <click selector="{{AdminUserGridSection.resetButton}}" stepKey="clickResetButton"/> <!--Create attribute set--> @@ -204,8 +202,7 @@ <dragAndDrop selector1="{{AdminNewAttributePanelSection.attributeName('cotton')}}" selector2="{{AdminNewAttributePanelSection.attributeName('fabric')}}" stepKey="selectMaterialsSimple"/> <!-- Save the product --> - <click selector="{{AdminProductFormActionSection.saveButton}}" stepKey="saveSimpleProduct"/> - <see selector="{{AdminProductMessagesSection.successMessage}}" userInput="You saved the product." stepKey="assertSuccessSimpleProduct"/> + <actionGroup ref="saveProductForm" stepKey="saveSimpleProduct"/> <!--Clear caches--> <magentoCLI command="cache:flush" stepKey="flushCache"/> diff --git a/app/code/Magento/Multishipping/Test/Mftf/Page/StorefrontMultishippingCheckoutAddressesPage.xml b/app/code/Magento/Multishipping/Test/Mftf/Page/StorefrontMultishippingCheckoutAddressesPage.xml new file mode 100644 index 0000000000000..b1f665f335f0a --- /dev/null +++ b/app/code/Magento/Multishipping/Test/Mftf/Page/StorefrontMultishippingCheckoutAddressesPage.xml @@ -0,0 +1,15 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<pages xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Page/etc/PageObject.xsd"> + <page name="StorefrontMultishippingCheckoutAddressesPage" url="/multishipping/checkout/addresses" area="storefront" + module="Magento_Multishipping"> + <section name="StorefrontMultishippingCheckoutSection"/> + </page> +</pages> diff --git a/app/code/Magento/Multishipping/Test/Mftf/Page/StorefrontMultishippingCheckoutShippingPage.xml b/app/code/Magento/Multishipping/Test/Mftf/Page/StorefrontMultishippingCheckoutShippingPage.xml new file mode 100644 index 0000000000000..98dbb7e355f27 --- /dev/null +++ b/app/code/Magento/Multishipping/Test/Mftf/Page/StorefrontMultishippingCheckoutShippingPage.xml @@ -0,0 +1,15 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<pages xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Page/etc/PageObject.xsd"> + <page name="StorefrontMultishippingCheckoutShippingPage" url="/multishipping/checkout/shipping" area="storefront" + module="Magento_Multishipping"> + <section name="StorefrontCheckoutGiftOptionsSection"/> + </page> +</pages> diff --git a/app/code/Magento/Multishipping/Test/Mftf/Section/StorefrontCheckoutCartSummarySection.xml b/app/code/Magento/Multishipping/Test/Mftf/Section/StorefrontCheckoutCartSummarySection.xml new file mode 100644 index 0000000000000..6d0707bb46d75 --- /dev/null +++ b/app/code/Magento/Multishipping/Test/Mftf/Section/StorefrontCheckoutCartSummarySection.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="StorefrontCheckoutCartSummarySection"> + <element name="checkoutWithMultipleAddresses" type="button" selector=".multicheckout" timeout="30"/> + </section> +</sections> diff --git a/app/code/Magento/Multishipping/Test/Mftf/Section/StorefrontMultishippingCheckoutSection.xml b/app/code/Magento/Multishipping/Test/Mftf/Section/StorefrontMultishippingCheckoutSection.xml new file mode 100644 index 0000000000000..6539c17de9e57 --- /dev/null +++ b/app/code/Magento/Multishipping/Test/Mftf/Section/StorefrontMultishippingCheckoutSection.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="StorefrontMultishippingCheckoutSection"> + <element name="goToShippingInformation" type="button" selector="button[title='Go to Shipping Information']" + timeout="30"/> + </section> +</sections> diff --git a/app/code/Magento/Multishipping/view/frontend/layout/multishipping_checkout_customer_address.xml b/app/code/Magento/Multishipping/view/frontend/layout/multishipping_checkout_customer_address.xml index c6bcdeb7b0413..fee3cb790a522 100644 --- a/app/code/Magento/Multishipping/view/frontend/layout/multishipping_checkout_customer_address.xml +++ b/app/code/Magento/Multishipping/view/frontend/layout/multishipping_checkout_customer_address.xml @@ -12,6 +12,7 @@ <block class="Magento\Customer\Block\Address\Edit" name="customer_address_edit" template="Magento_Customer::address/edit.phtml" cacheable="false"> <arguments> <argument name="attribute_data" xsi:type="object">Magento\Customer\Block\DataProviders\AddressAttributeData</argument> + <argument name="post_code_config" xsi:type="object">Magento\Customer\Block\DataProviders\PostCodesPatternsAttributeData</argument> </arguments> </block> </referenceContainer> diff --git a/app/code/Magento/Newsletter/Controller/Subscriber/NewAction.php b/app/code/Magento/Newsletter/Controller/Subscriber/NewAction.php index fd2a61702e909..ab4950b8a3452 100644 --- a/app/code/Magento/Newsletter/Controller/Subscriber/NewAction.php +++ b/app/code/Magento/Newsletter/Controller/Subscriber/NewAction.php @@ -77,9 +77,10 @@ public function __construct( protected function validateEmailAvailable($email) { $websiteId = $this->_storeManager->getStore()->getWebsiteId(); - if ($this->_customerSession->getCustomerDataObject()->getEmail() !== $email + if ($this->_customerSession->isLoggedIn() + && ($this->_customerSession->getCustomerDataObject()->getEmail() !== $email && !$this->customerAccountManagement->isEmailAvailable($email, $websiteId) - ) { + )) { throw new LocalizedException( __('This email address is already assigned to another user.') ); diff --git a/app/code/Magento/Newsletter/Model/ResourceModel/Subscriber.php b/app/code/Magento/Newsletter/Model/ResourceModel/Subscriber.php index 131eca5f33487..33e3826e9180b 100644 --- a/app/code/Magento/Newsletter/Model/ResourceModel/Subscriber.php +++ b/app/code/Magento/Newsletter/Model/ResourceModel/Subscriber.php @@ -132,43 +132,34 @@ public function loadByEmail($subscriberEmail) */ public function loadByCustomerData(\Magento\Customer\Api\Data\CustomerInterface $customer) { - $storeId = (int)$customer->getStoreId() ?: $this->storeManager - ->getWebsite($customer->getWebsiteId())->getDefaultStore()->getId(); - - $select = $this->connection - ->select() - ->from($this->getMainTable()) - ->where('customer_id=:customer_id and store_id=:store_id'); - - $result = $this->connection - ->fetchRow( - $select, - [ - 'customer_id' => $customer->getId(), - 'store_id' => $storeId - ] - ); + $storeIds = $this->storeManager->getWebsite($customer->getWebsiteId())->getStoreIds(); + + if ($customer->getId()) { + $select = $this->connection + ->select() + ->from($this->getMainTable()) + ->where('customer_id = ?', $customer->getId()) + ->where('store_id IN (?)', $storeIds); + + $result = $this->connection->fetchRow($select); - if ($result) { - return $result; + if ($result) { + return $result; + } } - $select = $this->connection - ->select() - ->from($this->getMainTable()) - ->where('subscriber_email=:subscriber_email and store_id=:store_id'); - - $result = $this->connection - ->fetchRow( - $select, - [ - 'subscriber_email' => $customer->getEmail(), - 'store_id' => $storeId - ] - ); + if ($customer->getEmail()) { + $select = $this->connection + ->select() + ->from($this->getMainTable()) + ->where('subscriber_email = ?', $customer->getEmail()) + ->where('store_id IN (?)', $storeIds); + + $result = $this->connection->fetchRow($select); - if ($result) { - return $result; + if ($result) { + return $result; + } } return []; diff --git a/app/code/Magento/Newsletter/Model/Subscriber.php b/app/code/Magento/Newsletter/Model/Subscriber.php index a1d929d8db7bf..2c6f494053efc 100644 --- a/app/code/Magento/Newsletter/Model/Subscriber.php +++ b/app/code/Magento/Newsletter/Model/Subscriber.php @@ -389,6 +389,9 @@ public function loadByCustomerId($customerId) try { $customerData = $this->customerRepository->getById($customerId); $customerData->setStoreId($this->_storeManager->getStore()->getId()); + if ($customerData->getWebsiteId() === null) { + $customerData->setWebsiteId($this->_storeManager->getStore()->getWebsiteId()); + } $data = $this->getResource()->loadByCustomerData($customerData); $this->addData($data); if (!empty($data) && $customerData->getId() && !$this->getCustomerId()) { @@ -549,7 +552,7 @@ public function updateSubscription($customerId) } /** - * Saving customer subscription status + * Saving customer subscription status. * * @param int $customerId * @param bool $subscribe indicates whether the customer should be subscribed or unsubscribed @@ -585,7 +588,12 @@ protected function _updateCustomerSubscription($customerId, $subscribe) if (AccountManagementInterface::ACCOUNT_CONFIRMATION_REQUIRED == $this->customerAccountManagement->getConfirmationStatus($customerId) ) { - $status = self::STATUS_UNCONFIRMED; + if ($this->getId() && $this->getStatus() == self::STATUS_SUBSCRIBED) { + // if a customer was already subscribed then keep the subscribed + $status = self::STATUS_SUBSCRIBED; + } else { + $status = self::STATUS_UNCONFIRMED; + } } elseif ($isConfirmNeed) { if ($this->getStatus() != self::STATUS_SUBSCRIBED) { $status = self::STATUS_NOT_ACTIVE; diff --git a/app/code/Magento/Newsletter/Test/Unit/Model/SubscriberTest.php b/app/code/Magento/Newsletter/Test/Unit/Model/SubscriberTest.php index 9c543c831ded3..0bf6f24a6b989 100644 --- a/app/code/Magento/Newsletter/Test/Unit/Model/SubscriberTest.php +++ b/app/code/Magento/Newsletter/Test/Unit/Model/SubscriberTest.php @@ -209,8 +209,14 @@ public function testSubscribeNotLoggedIn() $this->assertEquals(Subscriber::STATUS_NOT_ACTIVE, $this->subscriber->subscribe($email)); } + /** + * Update status with Confirmation Status - required. + * + * @return void + */ public function testUpdateSubscription() { + $websiteId = 1; $storeId = 2; $customerId = 1; $customerDataMock = $this->getMockBuilder(\Magento\Customer\Api\Data\CustomerInterface::class) @@ -224,7 +230,7 @@ public function testUpdateSubscription() ->willReturn( [ 'subscriber_id' => 1, - 'subscriber_status' => Subscriber::STATUS_SUBSCRIBED + 'subscriber_status' => Subscriber::STATUS_SUBSCRIBED, ] ); $customerDataMock->expects($this->atLeastOnce())->method('getId')->willReturn('id'); @@ -234,15 +240,19 @@ public function testUpdateSubscription() ->with($customerId) ->willReturn('account_confirmation_required'); $customerDataMock->expects($this->exactly(2))->method('getStoreId')->willReturn($storeId); + $customerDataMock->expects($this->exactly(2))->method('getWebsiteId')->willReturn(null); + $customerDataMock->expects($this->exactly(2))->method('setWebsiteId')->with($websiteId)->willReturnSelf(); $customerDataMock->expects($this->once())->method('getEmail')->willReturn('email'); $storeModel = $this->getMockBuilder(\Magento\Store\Model\Store::class) ->disableOriginalConstructor() - ->setMethods(['getId']) + ->setMethods(['getId', 'getWebsiteId']) ->getMock(); $this->storeManager->expects($this->any())->method('getStore')->willReturn($storeModel); + $storeModel->expects($this->exactly(2))->method('getWebsiteId')->willReturn($websiteId); + $data = $this->subscriber->updateSubscription($customerId); - $this->assertEquals($this->subscriber, $this->subscriber->updateSubscription($customerId)); + $this->assertEquals(Subscriber::STATUS_SUBSCRIBED, $data->getSubscriberStatus()); } public function testUnsubscribeCustomerById() diff --git a/app/code/Magento/Newsletter/etc/adminhtml/system.xml b/app/code/Magento/Newsletter/etc/adminhtml/system.xml index 1173f64310304..277005240eabc 100644 --- a/app/code/Magento/Newsletter/etc/adminhtml/system.xml +++ b/app/code/Magento/Newsletter/etc/adminhtml/system.xml @@ -11,39 +11,39 @@ <label>Newsletter</label> <tab>customer</tab> <resource>Magento_Newsletter::newsletter</resource> - <group id="subscription" translate="label" type="text" sortOrder="1" showInDefault="1" showInWebsite="1" showInStore="1"> + <group id="subscription" translate="label" type="text" sortOrder="10" showInDefault="1" showInWebsite="1" showInStore="1"> <label>Subscription Options</label> - <field id="allow_guest_subscribe" translate="label" type="select" sortOrder="1" showInDefault="1" showInWebsite="1" showInStore="1" canRestore="1"> + <field id="allow_guest_subscribe" translate="label" type="select" sortOrder="10" showInDefault="1" showInWebsite="1" showInStore="1" canRestore="1"> <label>Allow Guest Subscription</label> <source_model>Magento\Config\Model\Config\Source\Yesno</source_model> </field> - <field id="confirm" translate="label" type="select" sortOrder="1" showInDefault="1" showInWebsite="1" showInStore="1" canRestore="1"> + <field id="confirm" translate="label" type="select" sortOrder="20" showInDefault="1" showInWebsite="1" showInStore="1" canRestore="1"> <label>Need to Confirm</label> <source_model>Magento\Config\Model\Config\Source\Yesno</source_model> </field> - <field id="confirm_email_identity" translate="label" type="select" sortOrder="1" showInDefault="1" showInWebsite="1" showInStore="1" canRestore="1"> + <field id="confirm_email_identity" translate="label" type="select" sortOrder="30" showInDefault="1" showInWebsite="1" showInStore="1" canRestore="1"> <label>Confirmation Email Sender</label> <source_model>Magento\Config\Model\Config\Source\Email\Identity</source_model> </field> - <field id="confirm_email_template" translate="label comment" type="select" sortOrder="1" showInDefault="1" showInWebsite="1" showInStore="1" canRestore="1"> + <field id="confirm_email_template" translate="label comment" type="select" sortOrder="40" showInDefault="1" showInWebsite="1" showInStore="1" canRestore="1"> <label>Confirmation Email Template</label> <comment>Email template chosen based on theme fallback when "Default" option is selected.</comment> <source_model>Magento\Config\Model\Config\Source\Email\Template</source_model> </field> - <field id="success_email_identity" translate="label" type="select" sortOrder="1" showInDefault="1" showInWebsite="1" showInStore="1" canRestore="1"> + <field id="success_email_identity" translate="label" type="select" sortOrder="50" showInDefault="1" showInWebsite="1" showInStore="1" canRestore="1"> <label>Success Email Sender</label> <source_model>Magento\Config\Model\Config\Source\Email\Identity</source_model> </field> - <field id="success_email_template" translate="label comment" type="select" sortOrder="1" showInDefault="1" showInWebsite="1" showInStore="1" canRestore="1"> + <field id="success_email_template" translate="label comment" type="select" sortOrder="60" showInDefault="1" showInWebsite="1" showInStore="1" canRestore="1"> <label>Success Email Template</label> <comment>Email template chosen based on theme fallback when "Default" option is selected.</comment> <source_model>Magento\Config\Model\Config\Source\Email\Template</source_model> </field> - <field id="un_email_identity" translate="label" type="select" sortOrder="1" showInDefault="1" showInWebsite="1" showInStore="1" canRestore="1"> + <field id="un_email_identity" translate="label" type="select" sortOrder="70" showInDefault="1" showInWebsite="1" showInStore="1" canRestore="1"> <label>Unsubscription Email Sender</label> <source_model>Magento\Config\Model\Config\Source\Email\Identity</source_model> </field> - <field id="un_email_template" translate="label comment" type="select" sortOrder="1" showInDefault="1" showInWebsite="1" showInStore="1" canRestore="1"> + <field id="un_email_template" translate="label comment" type="select" sortOrder="80" showInDefault="1" showInWebsite="1" showInStore="1" canRestore="1"> <label>Unsubscription Email Template</label> <comment>Email template chosen based on theme fallback when "Default" option is selected.</comment> <source_model>Magento\Config\Model\Config\Source\Email\Template</source_model> diff --git a/app/code/Magento/Newsletter/view/adminhtml/templates/preview/iframeswitcher.phtml b/app/code/Magento/Newsletter/view/adminhtml/templates/preview/iframeswitcher.phtml index a64185ce67958..033bc799ba73e 100644 --- a/app/code/Magento/Newsletter/view/adminhtml/templates/preview/iframeswitcher.phtml +++ b/app/code/Magento/Newsletter/view/adminhtml/templates/preview/iframeswitcher.phtml @@ -16,7 +16,14 @@ </div> <?php endif;?> </div> - <iframe name="preview_iframe" id="preview_iframe" frameborder="0" title="<?= $block->escapeHtmlAttr(__('Preview')) ?>" width="100%"></iframe> + <iframe + name="preview_iframe" + id="preview_iframe" + frameborder="0" + title="<?= $block->escapeHtmlAttr(__('Preview')) ?>" + width="100%" + sandbox="allow-forms allow-pointer-lock allow-scripts"> + </iframe> <?= $block->getChildHtml('preview_form') ?> </div> diff --git a/app/code/Magento/Payment/Api/Data/PaymentAdditionalInfoInterface.php b/app/code/Magento/Payment/Api/Data/PaymentAdditionalInfoInterface.php new file mode 100644 index 0000000000000..c658baece7779 --- /dev/null +++ b/app/code/Magento/Payment/Api/Data/PaymentAdditionalInfoInterface.php @@ -0,0 +1,16 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +namespace Magento\Payment\Api\Data; + +use Magento\Framework\DataObject\KeyValueObjectInterface; + +/** + * Payment additional info interface. + */ +interface PaymentAdditionalInfoInterface extends KeyValueObjectInterface +{ +} diff --git a/app/code/Magento/Payment/Model/PaymentAdditionalInfo.php b/app/code/Magento/Payment/Model/PaymentAdditionalInfo.php new file mode 100644 index 0000000000000..4ce41181a008a --- /dev/null +++ b/app/code/Magento/Payment/Model/PaymentAdditionalInfo.php @@ -0,0 +1,60 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Payment\Model; + +use Magento\Payment\Api\Data\PaymentAdditionalInfoInterface; + +/** + * Payment additional info class. + */ +class PaymentAdditionalInfo implements PaymentAdditionalInfoInterface +{ + /** + * @var string + */ + private $key; + + /** + * @var string + */ + private $value; + + /** + * @inheritdoc + */ + public function getKey() + { + return $this->key; + } + + /** + * @inheritdoc + */ + public function getValue() + { + return $this->value; + } + + /** + * @inheritdoc + */ + public function setKey($key) + { + $this->key = $key; + return $this; + } + + /** + * @inheritdoc + */ + public function setValue($value) + { + $this->value = $value; + return $this; + } +} diff --git a/app/code/Magento/Payment/etc/di.xml b/app/code/Magento/Payment/etc/di.xml index 74f553cc64094..b7422bb00d543 100644 --- a/app/code/Magento/Payment/etc/di.xml +++ b/app/code/Magento/Payment/etc/di.xml @@ -7,6 +7,7 @@ --> <config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:framework:ObjectManager/etc/config.xsd"> <preference for="Magento\Payment\Api\Data\PaymentMethodInterface" type="Magento\Payment\Model\PaymentMethod"/> + <preference for="Magento\Payment\Api\Data\PaymentAdditionalInfoInterface" type="Magento\Payment\Model\PaymentAdditionalInfo"/> <preference for="Magento\Payment\Api\PaymentMethodListInterface" type="Magento\Payment\Model\PaymentMethodList"/> <preference for="Magento\Payment\Gateway\Validator\ResultInterface" type="Magento\Payment\Gateway\Validator\Result"/> <preference for="Magento\Payment\Gateway\ConfigFactoryInterface" type="Magento\Payment\Gateway\Config\ConfigFactory" /> 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 163aaf979ec2e..afa71fe591495 100644 --- a/app/code/Magento/Payment/view/frontend/templates/transparent/iframe.phtml +++ b/app/code/Magento/Payment/view/frontend/templates/transparent/iframe.phtml @@ -40,7 +40,7 @@ $params = $block->getParams(); $(parent).trigger('clearTimeout'); fullScreenLoader.stopLoader(); globalMessageList.addErrorMessage({ - message: $t('An error occurred on the server. Please try to place the order again.') + message: $t(<?= /* @escapeNotVerified */ json_encode($params['error_msg'])?>) }); } ); diff --git a/app/code/Magento/Paypal/Controller/Express/AbstractExpress.php b/app/code/Magento/Paypal/Controller/Express/AbstractExpress.php index 571d73d07b68e..95804ebe45d8c 100644 --- a/app/code/Magento/Paypal/Controller/Express/AbstractExpress.php +++ b/app/code/Magento/Paypal/Controller/Express/AbstractExpress.php @@ -137,6 +137,14 @@ protected function _initCheckout() $this->getResponse()->setStatusHeader(403, '1.1', 'Forbidden'); throw new \Magento\Framework\Exception\LocalizedException(__('We can\'t initialize Express Checkout.')); } + if (!(float)$quote->getGrandTotal()) { + throw new \Magento\Framework\Exception\LocalizedException( + __( + 'PayPal can\'t process orders with a zero balance due. ' + . 'To finish your purchase, please go through the standard checkout process.' + ) + ); + } if (!isset($this->_checkoutTypes[$this->_checkoutType])) { $parameters = [ 'params' => [ @@ -151,6 +159,8 @@ protected function _initCheckout() } /** + * Get proper checkout token. + * * Search for proper checkout token in request or session or (un)set specified one * Combined getter/setter * @@ -221,8 +231,7 @@ protected function _getQuote() } /** - * Returns before_auth_url redirect parameter for customer session - * @return null + * @inheritdoc */ public function getCustomerBeforeAuthUrl() { @@ -230,8 +239,7 @@ public function getCustomerBeforeAuthUrl() } /** - * Returns a list of action flags [flag_key] => boolean - * @return array + * @inheritdoc */ public function getActionFlagList() { @@ -240,6 +248,7 @@ public function getActionFlagList() /** * Returns login url parameter for redirect + * * @return string */ public function getLoginUrl() @@ -249,6 +258,7 @@ public function getLoginUrl() /** * Returns action name which requires redirect + * * @return string */ public function getRedirectActionName() diff --git a/app/code/Magento/Paypal/Model/Config/Structure/Element/FieldPlugin.php b/app/code/Magento/Paypal/Model/Config/Structure/Element/FieldPlugin.php index c2056aea08c00..5d5db0128b1eb 100644 --- a/app/code/Magento/Paypal/Model/Config/Structure/Element/FieldPlugin.php +++ b/app/code/Magento/Paypal/Model/Config/Structure/Element/FieldPlugin.php @@ -5,7 +5,6 @@ */ namespace Magento\Paypal\Model\Config\Structure\Element; -use Magento\Framework\App\RequestInterface; use Magento\Config\Model\Config\Structure\Element\Field as FieldConfigStructure; use Magento\Paypal\Model\Config\StructurePlugin as ConfigStructurePlugin; @@ -14,19 +13,6 @@ */ class FieldPlugin { - /** - * @var RequestInterface - */ - private $request; - - /** - * @param RequestInterface $request - */ - public function __construct(RequestInterface $request) - { - $this->request = $request; - } - /** * Get original configPath (not changed by PayPal configuration inheritance) * @@ -36,7 +22,7 @@ public function __construct(RequestInterface $request) */ public function afterGetConfigPath(FieldConfigStructure $subject, $result) { - if (!$result && $this->request->getParam('section') == 'payment') { + if (!$result && strpos($subject->getPath(), 'payment_') === 0) { $result = preg_replace( '@^(' . implode('|', ConfigStructurePlugin::getPaypalConfigCountries(true)) . ')/@', 'payment/', diff --git a/app/code/Magento/Paypal/Model/Express.php b/app/code/Magento/Paypal/Model/Express.php index 196e59c6593b9..a44fb9e7c15b0 100644 --- a/app/code/Magento/Paypal/Model/Express.php +++ b/app/code/Magento/Paypal/Model/Express.php @@ -44,7 +44,7 @@ class Express extends \Magento\Payment\Model\Method\AbstractMethod * * @var bool */ - protected $_isGateway = false; + protected $_isGateway = true; /** * Availability option diff --git a/app/code/Magento/Paypal/Test/Unit/Model/Config/Structure/Element/FieldPluginTest.php b/app/code/Magento/Paypal/Test/Unit/Model/Config/Structure/Element/FieldPluginTest.php index 8615b91383aaa..72c13c80b31e4 100644 --- a/app/code/Magento/Paypal/Test/Unit/Model/Config/Structure/Element/FieldPluginTest.php +++ b/app/code/Magento/Paypal/Test/Unit/Model/Config/Structure/Element/FieldPluginTest.php @@ -7,7 +7,6 @@ use Magento\Paypal\Model\Config\Structure\Element\FieldPlugin as FieldConfigStructurePlugin; use Magento\Framework\TestFramework\Unit\Helper\ObjectManager as ObjectManagerHelper; -use Magento\Framework\App\RequestInterface; use Magento\Config\Model\Config\Structure\Element\Field as FieldConfigStructureMock; class FieldPluginTest extends \PHPUnit\Framework\TestCase @@ -22,11 +21,6 @@ class FieldPluginTest extends \PHPUnit\Framework\TestCase */ private $objectManagerHelper; - /** - * @var RequestInterface|\PHPUnit_Framework_MockObject_MockObject - */ - private $requestMock; - /** * @var FieldConfigStructureMock|\PHPUnit_Framework_MockObject_MockObject */ @@ -34,16 +28,13 @@ class FieldPluginTest extends \PHPUnit\Framework\TestCase protected function setUp() { - $this->requestMock = $this->getMockBuilder(RequestInterface::class) - ->getMockForAbstractClass(); $this->subjectMock = $this->getMockBuilder(FieldConfigStructureMock::class) ->disableOriginalConstructor() ->getMock(); $this->objectManagerHelper = new ObjectManagerHelper($this); $this->plugin = $this->objectManagerHelper->getObject( - FieldConfigStructurePlugin::class, - ['request' => $this->requestMock] + FieldConfigStructurePlugin::class ); } @@ -56,10 +47,8 @@ public function testAroundGetConfigPathHasResult() public function testAroundGetConfigPathNonPaymentSection() { - $this->requestMock->expects(static::once()) - ->method('getParam') - ->with('section') - ->willReturn('non-payment'); + $this->subjectMock->method('getPath') + ->willReturn('non-payment/group/field'); $this->assertNull($this->plugin->afterGetConfigPath($this->subjectMock, null)); } @@ -72,12 +61,7 @@ public function testAroundGetConfigPathNonPaymentSection() */ public function testAroundGetConfigPath($subjectPath, $expectedConfigPath) { - $this->requestMock->expects(static::once()) - ->method('getParam') - ->with('section') - ->willReturn('payment'); - $this->subjectMock->expects(static::once()) - ->method('getPath') + $this->subjectMock->method('getPath') ->willReturn($subjectPath); $this->assertEquals($expectedConfigPath, $this->plugin->afterGetConfigPath($this->subjectMock, null)); diff --git a/app/code/Magento/Persistent/Block/Header/Additional.php b/app/code/Magento/Persistent/Block/Header/Additional.php index c740f5a3469fb..28e967f201230 100644 --- a/app/code/Magento/Persistent/Block/Header/Additional.php +++ b/app/code/Magento/Persistent/Block/Header/Additional.php @@ -5,6 +5,10 @@ */ namespace Magento\Persistent\Block\Header; +use Magento\Framework\App\ObjectManager; +use Magento\Framework\Serialize\Serializer\Json; +use Magento\Persistent\Helper\Data; + /** * Remember Me block * @@ -30,20 +34,37 @@ class Additional extends \Magento\Framework\View\Element\Html\Link protected $customerRepository; /** - * Constructor - * + * @var string + */ + protected $_template = 'Magento_Persistent::additional.phtml'; + + /** + * @var Json + */ + private $jsonSerializer; + + /** + * @var Data + */ + private $persistentHelper; + + /** * @param \Magento\Framework\View\Element\Template\Context $context * @param \Magento\Customer\Helper\View $customerViewHelper * @param \Magento\Persistent\Helper\Session $persistentSessionHelper * @param \Magento\Customer\Api\CustomerRepositoryInterface $customerRepository * @param array $data + * @param Json|null $jsonSerializer + * @param Data|null $persistentHelper */ public function __construct( \Magento\Framework\View\Element\Template\Context $context, \Magento\Customer\Helper\View $customerViewHelper, \Magento\Persistent\Helper\Session $persistentSessionHelper, \Magento\Customer\Api\CustomerRepositoryInterface $customerRepository, - array $data = [] + array $data = [], + Json $jsonSerializer = null, + Data $persistentHelper = null ) { $this->isScopePrivate = true; $this->_customerViewHelper = $customerViewHelper; @@ -51,6 +72,8 @@ public function __construct( $this->customerRepository = $customerRepository; parent::__construct($context, $data); $this->_isScopePrivate = true; + $this->jsonSerializer = $jsonSerializer ?: ObjectManager::getInstance()->get(Json::class); + $this->persistentHelper = $persistentHelper ?: ObjectManager::getInstance()->get(Data::class); } /** @@ -64,17 +87,25 @@ public function getHref() } /** - * Render additional header html + * @return int + */ + public function getCustomerId() + { + return $this->_persistentSessionHelper->getSession()->getCustomerId(); + } + + /** + * Get persistent config. * * @return string */ - protected function _toHtml() + public function getConfig() { - if ($this->_persistentSessionHelper->getSession()->getCustomerId()) { - return '<span><a ' . $this->getLinkAttributes() . ' >' . __('Not you?') - . '</a></span>'; - } - - return ''; + return + $this->jsonSerializer->serialize( + [ + 'expirationLifetime' => $this->persistentHelper->getLifeTime(), + ] + ); } } diff --git a/app/code/Magento/Persistent/CustomerData/Persistent.php b/app/code/Magento/Persistent/CustomerData/Persistent.php new file mode 100644 index 0000000000000..98b36b2e36612 --- /dev/null +++ b/app/code/Magento/Persistent/CustomerData/Persistent.php @@ -0,0 +1,71 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +namespace Magento\Persistent\CustomerData; + +use Magento\Customer\Api\CustomerRepositoryInterface; +use Magento\Customer\CustomerData\SectionSourceInterface; +use Magento\Customer\Helper\View; +use Magento\Persistent\Helper\Session; + +/** + * Customer persistent section + */ +class Persistent implements SectionSourceInterface +{ + /** + * @var Session + */ + private $persistentSession; + + /** + * @var View + */ + private $customerViewHelper; + + /** + * @var CustomerRepositoryInterface + */ + private $customerRepository; + + /** + * @param Session $persistentSession + * @param View $customerViewHelper + * @param CustomerRepositoryInterface $customerRepository + */ + public function __construct( + Session $persistentSession, + View $customerViewHelper, + CustomerRepositoryInterface $customerRepository + ) { + $this->persistentSession = $persistentSession; + $this->customerViewHelper = $customerViewHelper; + $this->customerRepository = $customerRepository; + } + + /** + * Get data + * + * @return array + */ + public function getSectionData() + { + if (!$this->persistentSession->isPersistent()) { + return []; + } + + $customerId = $this->persistentSession->getSession()->getCustomerId(); + if (!$customerId) { + return []; + } + + $customer = $this->customerRepository->getById($customerId); + + return [ + 'fullname' => $this->customerViewHelper->getCustomerName($customer), + ]; + } +} diff --git a/app/code/Magento/Persistent/Model/Checkout/GuestPaymentInformationManagementPlugin.php b/app/code/Magento/Persistent/Model/Checkout/GuestPaymentInformationManagementPlugin.php index 2c0259bd12b00..2641102ca4d72 100644 --- a/app/code/Magento/Persistent/Model/Checkout/GuestPaymentInformationManagementPlugin.php +++ b/app/code/Magento/Persistent/Model/Checkout/GuestPaymentInformationManagementPlugin.php @@ -108,8 +108,8 @@ public function beforeSavePaymentInformationAndPlaceOrder( $this->customerSession->setCustomerId(null); $this->customerSession->setCustomerGroupId(null); $this->quoteManager->convertCustomerCartToGuest(); - /** @var \Magento\Quote\Api\Data\CartInterface $quote */ - $quote = $this->cartRepository->get($this->checkoutSession->getQuote()->getId()); + $quoteId = $this->checkoutSession->getQuoteId(); + $quote = $this->cartRepository->get($quoteId); $quote->setCustomerEmail($email); $quote->getAddressesCollection()->walk('setEmail', ['email' => $email]); $this->cartRepository->save($quote); diff --git a/app/code/Magento/Persistent/Model/Observer.php b/app/code/Magento/Persistent/Model/Observer.php index 53fe5f95531e1..81c2870071a2e 100644 --- a/app/code/Magento/Persistent/Model/Observer.php +++ b/app/code/Magento/Persistent/Model/Observer.php @@ -86,13 +86,8 @@ public function __construct( */ public function emulateWelcomeBlock($block) { - $customerName = $this->_customerViewHelper->getCustomerName( - $this->customerRepository->getById($this->_persistentSession->getSession()->getCustomerId()) - ); + $block->setWelcome(' '); - $this->_applyAccountLinksPersistentData(); - $welcomeMessage = __('Welcome, %1!', $customerName); - $block->setWelcome($welcomeMessage); return $this; } diff --git a/app/code/Magento/Persistent/Model/Plugin/PersistentCustomerContext.php b/app/code/Magento/Persistent/Model/Plugin/PersistentCustomerContext.php new file mode 100644 index 0000000000000..04b4450d93cec --- /dev/null +++ b/app/code/Magento/Persistent/Model/Plugin/PersistentCustomerContext.php @@ -0,0 +1,39 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +namespace Magento\Persistent\Model\Plugin; + +/** + * Plugin for Magento\Framework\App\Http\Context to create new page cache variation for persistent session. + */ +class PersistentCustomerContext +{ + /** + * Persistent session + * + * @var \Magento\Persistent\Helper\Session + */ + private $persistentSession; + + /** + * @param \Magento\Persistent\Helper\Session $persistentSession + */ + public function __construct( + \Magento\Persistent\Helper\Session $persistentSession + ) { + $this->persistentSession = $persistentSession; + } + + /** + * @param \Magento\Framework\App\Http\Context $subject + * @return mixed + */ + public function beforeGetVaryString(\Magento\Framework\App\Http\Context $subject) + { + if ($this->persistentSession->isPersistent()) { + $subject->setValue('PERSISTENT', 1, 0); + } + } +} diff --git a/app/code/Magento/Persistent/Model/QuoteManager.php b/app/code/Magento/Persistent/Model/QuoteManager.php index 8937a4920cb23..35c2c70be30dc 100644 --- a/app/code/Magento/Persistent/Model/QuoteManager.php +++ b/app/code/Magento/Persistent/Model/QuoteManager.php @@ -108,8 +108,9 @@ public function setGuest($checkQuote = false) */ public function convertCustomerCartToGuest() { + $quoteId = $this->checkoutSession->getQuoteId(); /** @var $quote \Magento\Quote\Model\Quote */ - $quote = $this->quoteRepository->get($this->checkoutSession->getQuote()->getId()); + $quote = $this->quoteRepository->get($quoteId); if ($quote && $quote->getId()) { $this->_setQuotePersistent = false; $quote->setIsActive(true) diff --git a/app/code/Magento/Persistent/Test/Mftf/ActionGroup/StorefrontCustomerActionGroup.xml b/app/code/Magento/Persistent/Test/Mftf/ActionGroup/StorefrontCustomerActionGroup.xml new file mode 100644 index 0000000000000..2d8c1a174b013 --- /dev/null +++ b/app/code/Magento/Persistent/Test/Mftf/ActionGroup/StorefrontCustomerActionGroup.xml @@ -0,0 +1,22 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> + <actionGroup name="CustomerLoginOnStorefrontWithRememberMeChecked" extends="CustomerLoginOnStorefront"> + <checkOption selector="{{StorefrontCustomerSignInFormSection.rememberMe}}" + before="clickSignInAccountButton" + stepKey="checkRememberMe"/> + </actionGroup> + + <actionGroup name="CustomerLoginOnStorefrontWithRememberMeUnChecked" extends="CustomerLoginOnStorefront"> + <uncheckOption selector="{{StorefrontCustomerSignInFormSection.rememberMe}}" + before="clickSignInAccountButton" + stepKey="unCheckRememberMe"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/Persistent/Test/Mftf/Data/PersistentData.xml b/app/code/Magento/Persistent/Test/Mftf/Data/PersistentData.xml index a23d1169b6865..df8fb91bdabc6 100644 --- a/app/code/Magento/Persistent/Test/Mftf/Data/PersistentData.xml +++ b/app/code/Magento/Persistent/Test/Mftf/Data/PersistentData.xml @@ -20,4 +20,18 @@ <entity name="persistentEnabledState" type="persistent_options_enabled"> <data key="value">1</data> </entity> + + <entity name="PersistentLogoutClearEnabled" type="persistent_config_state"> + <requiredEntity type="persistent_options_logout_clear">LogoutClearActive</requiredEntity> + </entity> + <entity name="LogoutClearActive" type="logout_clear"> + <data key="value">1</data> + </entity> + + <entity name="PersistentLogoutClearDisabled" type="persistent_config_state"> + <requiredEntity type="persistent_options_logout_clear">LogoutClearInactive</requiredEntity> + </entity> + <entity name="LogoutClearInactive" type="logout_clear"> + <data key="value">0</data> + </entity> </entities> diff --git a/app/code/Magento/Persistent/Test/Mftf/Metadata/perstistent_config-meta.xml b/app/code/Magento/Persistent/Test/Mftf/Metadata/perstistent_config-meta.xml index 7a84b5deba68f..dc9d0eddc09c3 100644 --- a/app/code/Magento/Persistent/Test/Mftf/Metadata/perstistent_config-meta.xml +++ b/app/code/Magento/Persistent/Test/Mftf/Metadata/perstistent_config-meta.xml @@ -14,6 +14,9 @@ <object key="enabled" dataType="persistent_options_enabled"> <field key="value">string</field> </object> + <object key="logout_clear" dataType="persistent_options_logout_clear"> + <field key="value">string</field> + </object> </object> </object> </object> diff --git a/app/code/Magento/Persistent/Test/Mftf/Section/StorefrontCustomerSignInFormSection.xml b/app/code/Magento/Persistent/Test/Mftf/Section/StorefrontCustomerSignInFormSection.xml new file mode 100644 index 0000000000000..c2220c33a6052 --- /dev/null +++ b/app/code/Magento/Persistent/Test/Mftf/Section/StorefrontCustomerSignInFormSection.xml @@ -0,0 +1,14 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<sections xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Page/etc/SectionObject.xsd"> + <section name="StorefrontCustomerSignInFormSection"> + <element name="rememberMe" type="checkbox" selector="[name='persistent_remember_me']"/> + </section> +</sections> diff --git a/app/code/Magento/Persistent/Test/Mftf/Test/StorefrontCorrectWelcomeMessageAfterCustomerIsLoggedOutTest.xml b/app/code/Magento/Persistent/Test/Mftf/Test/StorefrontCorrectWelcomeMessageAfterCustomerIsLoggedOutTest.xml new file mode 100644 index 0000000000000..785fc66a4929a --- /dev/null +++ b/app/code/Magento/Persistent/Test/Mftf/Test/StorefrontCorrectWelcomeMessageAfterCustomerIsLoggedOutTest.xml @@ -0,0 +1,79 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> + <test name="StorefrontCorrectWelcomeMessageAfterCustomerIsLoggedOutTest"> + <annotations> + <features value="Persistent"/> + <stories value="MAGETWO-95850 - Incorrect use of cookies for customer"/> + <title value="Checking welcome message for persistent customer after logout"/> + <description value="Checking welcome message for persistent customer after logout"/> + <severity value="MAJOR"/> + <testCaseId value="MAGETWO-97095"/> + <group value="persistent"/> + <group value="customer"/> + </annotations> + <before> + <!--Enable Persistence--> + <createData entity="PersistentConfigEnabled" stepKey="enablePersistent"/> + <createData entity="PersistentLogoutClearDisabled" stepKey="persistentLogoutClearDisable"/> + + <!--Create customers--> + <createData entity="Simple_US_Customer" stepKey="createCustomer"/> + <createData entity="Simple_US_Customer" stepKey="createCustomerForPersistent"> + <field key="firstname">John1</field> + <field key="lastname">Doe1</field> + </createData> + </before> + <after> + <!--Roll back configuration--> + <createData entity="PersistentConfigDefault" stepKey="setDefaultPersistentState"/> + <createData entity="PersistentLogoutClearEnabled" stepKey="persistentLogoutClearEnabled"/> + + <!-- Logout customer on Storefront--> + <actionGroup ref="CustomerLogoutStorefrontActionGroup" stepKey="customerLogoutStorefront"/> + <!--Delete customers--> + <deleteData createDataKey="createCustomer" stepKey="deleteCustomer"/> + <deleteData createDataKey="createCustomerForPersistent" stepKey="deleteCustomerForPersistent"/> + </after> + <!--Login as a Customer with remember me unchecked--> + <actionGroup ref="CustomerLoginOnStorefrontWithRememberMeUnChecked" stepKey="loginToStorefrontAccountWithRememberMeUnchecked"> + <argument name="customer" value="$$createCustomer$$"/> + </actionGroup> + + <!--Check customer name and last name in welcome message--> + <seeCurrentUrlMatches regex="~/customer/account/~" stepKey="seeCustomerAccountPageUrl"/> + <see userInput="Welcome, $$createCustomer.firstname$$ $$createCustomer.lastname$$!" + selector="{{StorefrontHeaderSection.welcomeMessage}}" + stepKey="seeLoggedInCustomerWelcomeMessage"/> + <!--Logout and check default welcome message--> + <actionGroup ref="CustomerLogoutStorefrontByMenuItemsActionGroup" stepKey="storefrontCustomerLogout"/> + <seeCurrentUrlMatches regex="~/customer/account/logoutSuccess/~" wait="5" stepKey="seeCustomerSignOutPageUrl"/> + <see userInput="Default welcome msg!" + selector="{{StorefrontHeaderSection.welcomeMessage}}" + stepKey="seeDefaultWelcomeMessage"/> + + <!--Login as a Customer with remember me checked--> + <actionGroup ref="CustomerLoginOnStorefrontWithRememberMeChecked" stepKey="loginToStorefrontAccountWithRememberMeChecked"> + <argument name="customer" value="$$createCustomerForPersistent$$"/> + </actionGroup> + <!--Check customer name and last name in welcome message--> + <seeCurrentUrlMatches regex="~/customer/account/~" stepKey="seeCustomerAccountPageUrl1"/> + <see userInput="Welcome, $$createCustomerForPersistent.firstname$$ $$createCustomerForPersistent.lastname$$!" + selector="{{StorefrontHeaderSection.welcomeMessage}}" + stepKey="seeLoggedInCustomerWelcomeMessage1"/> + + <!--Logout and check persistent customer welcome message--> + <actionGroup ref="CustomerLogoutStorefrontByMenuItemsActionGroup" stepKey="storefrontCustomerLogout1"/> + <seeCurrentUrlMatches regex="~/customer/account/logoutSuccess/~" wait="5" stepKey="seeCustomerSignOutPageUrl1"/> + <see userInput="Welcome, $$createCustomerForPersistent.firstname$$ $$createCustomerForPersistent.lastname$$! Not you?" + selector="{{StorefrontHeaderSection.welcomeMessage}}" + stepKey="seePersistentWelcomeMessage"/> + </test> +</tests> diff --git a/app/code/Magento/Persistent/Test/Unit/Block/Header/AdditionalTest.php b/app/code/Magento/Persistent/Test/Unit/Block/Header/AdditionalTest.php index b88b02ab4cfb5..a6ad8b1aaab33 100644 --- a/app/code/Magento/Persistent/Test/Unit/Block/Header/AdditionalTest.php +++ b/app/code/Magento/Persistent/Test/Unit/Block/Header/AdditionalTest.php @@ -34,44 +34,14 @@ class AdditionalTest extends \PHPUnit\Framework\TestCase protected $contextMock; /** - * @var \Magento\Framework\Event\ManagerInterface|\PHPUnit_Framework_MockObject_MockObject + * @var \Magento\Framework\Serialize\Serializer\Json|\PHPUnit_Framework_MockObject_MockObject */ - protected $eventManagerMock; + private $jsonSerializerMock; /** - * @var \Magento\Framework\App\Config\ScopeConfigInterface|\PHPUnit_Framework_MockObject_MockObject + * @var \Magento\Persistent\Helper\Data|\PHPUnit_Framework_MockObject_MockObject */ - protected $scopeConfigMock; - - /** - * @var \Magento\Framework\App\Cache\StateInterface|\PHPUnit_Framework_MockObject_MockObject - */ - protected $cacheStateMock; - - /** - * @var \Magento\Framework\App\CacheInterface|\PHPUnit_Framework_MockObject_MockObject - */ - protected $cacheMock; - - /** - * @var \Magento\Framework\Session\SidResolverInterface|\PHPUnit_Framework_MockObject_MockObject - */ - protected $sidResolverMock; - - /** - * @var \Magento\Framework\Session\SessionManagerInterface|\PHPUnit_Framework_MockObject_MockObject - */ - protected $sessionMock; - - /** - * @var \Magento\Framework\Escaper|\PHPUnit_Framework_MockObject_MockObject - */ - protected $escaperMock; - - /** - * @var \Magento\Framework\UrlInterface|\PHPUnit_Framework_MockObject_MockObject - */ - protected $urlBuilderMock; + private $persistentHelperMock; /** * @var \Magento\Persistent\Block\Header\Additional @@ -93,17 +63,7 @@ protected function setUp() { $this->objectManager = new \Magento\Framework\TestFramework\Unit\Helper\ObjectManager($this); - $this->contextMock = $this->createPartialMock(\Magento\Framework\View\Element\Template\Context::class, [ - 'getEventManager', - 'getScopeConfig', - 'getCacheState', - 'getCache', - 'getInlineTranslation', - 'getSidResolver', - 'getSession', - 'getEscaper', - 'getUrlBuilder' - ]); + $this->contextMock = $this->createPartialMock(\Magento\Framework\View\Element\Template\Context::class, []); $this->customerViewHelperMock = $this->createMock(\Magento\Customer\Helper\View::class); $this->persistentSessionHelperMock = $this->createPartialMock( \Magento\Persistent\Helper\Session::class, @@ -119,103 +79,14 @@ protected function setUp() ['getById'] ); - $this->eventManagerMock = $this->getMockForAbstractClass( - \Magento\Framework\Event\ManagerInterface::class, - [], - '', - false, - true, - true, - ['dispatch'] - ); - $this->scopeConfigMock = $this->getMockForAbstractClass( - \Magento\Framework\App\Config\ScopeConfigInterface::class, - [], - '', - false, - true, - true, - ['getValue'] - ); - $this->cacheStateMock = $this->getMockForAbstractClass( - \Magento\Framework\App\Cache\StateInterface::class, - [], - '', - false, - true, - true, - ['isEnabled'] + $this->jsonSerializerMock = $this->createPartialMock( + \Magento\Framework\Serialize\Serializer\Json::class, + ['serialize'] ); - $this->cacheMock = $this->getMockForAbstractClass( - \Magento\Framework\App\CacheInterface::class, - [], - '', - false, - true, - true, - ['load'] + $this->persistentHelperMock = $this->createPartialMock( + \Magento\Persistent\Helper\Data::class, + ['getLifeTime'] ); - $this->sidResolverMock = $this->getMockForAbstractClass( - \Magento\Framework\Session\SidResolverInterface::class, - [], - '', - false, - true, - true, - ['getSessionIdQueryParam'] - ); - $this->sessionMock = $this->getMockForAbstractClass( - \Magento\Framework\Session\SessionManagerInterface::class, - [], - '', - false, - true, - true, - ['getSessionId'] - ); - $this->escaperMock = $this->getMockForAbstractClass( - \Magento\Framework\Escaper::class, - [], - '', - false, - true, - true, - ['escapeHtml'] - ); - $this->urlBuilderMock = $this->getMockForAbstractClass( - \Magento\Framework\UrlInterface::class, - [], - '', - false, - true, - true, - ['getUrl'] - ); - - $this->contextMock->expects($this->once()) - ->method('getEventManager') - ->willReturn($this->eventManagerMock); - $this->contextMock->expects($this->once()) - ->method('getScopeConfig') - ->willReturn($this->scopeConfigMock); - $this->contextMock->expects($this->once()) - ->method('getCacheState') - ->willReturn($this->cacheStateMock); - $this->contextMock->expects($this->once()) - ->method('getCache') - ->willReturn($this->cacheMock); - $this->contextMock->expects($this->once()) - ->method('getSidResolver') - ->willReturn($this->sidResolverMock); - $this->contextMock->expects($this->once()) - ->method('getSession') - ->willReturn($this->sessionMock); - $this->contextMock->expects($this->once()) - ->method('getEscaper') - ->willReturn($this->escaperMock); - $this->contextMock->expects($this->once()) - ->method('getUrlBuilder') - ->willReturn($this->urlBuilderMock); $this->additional = $this->objectManager->getObject( \Magento\Persistent\Block\Header\Additional::class, @@ -224,91 +95,48 @@ protected function setUp() 'customerViewHelper' => $this->customerViewHelperMock, 'persistentSessionHelper' => $this->persistentSessionHelperMock, 'customerRepository' => $this->customerRepositoryMock, - 'data' => [] + 'data' => [], + 'jsonSerializer' => $this->jsonSerializerMock, + 'persistentHelper' => $this->persistentHelperMock, ] ); } /** - * Run test toHtml method - * - * @param bool $customerId * @return void - * - * @dataProvider dataProviderToHtml */ - public function testToHtml($customerId) + public function testGetCustomerId() { - $cacheData = false; - $idQueryParam = 'id-query-param'; - $sessionId = 'session-id'; - - $this->additional->setData('cache_lifetime', 789); - $this->additional->setData('cache_key', 'cache-key'); - - $this->eventManagerMock->expects($this->at(0)) - ->method('dispatch') - ->with('view_block_abstract_to_html_before', ['block' => $this->additional]); - $this->eventManagerMock->expects($this->at(1)) - ->method('dispatch') - ->with('view_block_abstract_to_html_after'); - $this->scopeConfigMock->expects($this->once()) - ->method('getValue') - ->with( - 'advanced/modules_disable_output/Magento_Persistent', - \Magento\Store\Model\ScopeInterface::SCOPE_STORE - )->willReturn(false); - - // get cache - $this->cacheStateMock->expects($this->at(0)) - ->method('isEnabled') - ->with(\Magento\Persistent\Block\Header\Additional::CACHE_GROUP) - ->willReturn(true); - // save cache - $this->cacheStateMock->expects($this->at(1)) - ->method('isEnabled') - ->with(\Magento\Persistent\Block\Header\Additional::CACHE_GROUP) - ->willReturn(false); - - $this->cacheMock->expects($this->once()) - ->method('load') - ->willReturn($cacheData); - $this->sidResolverMock->expects($this->never()) - ->method('getSessionIdQueryParam') - ->with($this->sessionMock) - ->willReturn($idQueryParam); - $this->sessionMock->expects($this->never()) - ->method('getSessionId') - ->willReturn($sessionId); - - // call protected _toHtml method + $customerId = 1; + /** @var \Magento\Persistent\Model\Session|\PHPUnit_Framework_MockObject_MockObject $sessionMock */ $sessionMock = $this->createPartialMock(\Magento\Persistent\Model\Session::class, ['getCustomerId']); - - $this->persistentSessionHelperMock->expects($this->atLeastOnce()) - ->method('getSession') - ->willReturn($sessionMock); - - $sessionMock->expects($this->atLeastOnce()) + $sessionMock->expects($this->once()) ->method('getCustomerId') ->willReturn($customerId); + $this->persistentSessionHelperMock->expects($this->once()) + ->method('getSession') + ->willReturn($sessionMock); - if ($customerId) { - $this->assertEquals('<span><a >Not you?</a></span>', $this->additional->toHtml()); - } else { - $this->assertEquals('', $this->additional->toHtml()); - } + $this->assertEquals($customerId, $this->additional->getCustomerId()); } /** - * Data provider for dataProviderToHtml method - * - * @return array + * @return void */ - public function dataProviderToHtml() + public function testGetConfig() { - return [ - ['customerId' => 2], - ['customerId' => null], - ]; + $lifeTime = 500; + $arrayToSerialize = ['expirationLifetime' => $lifeTime]; + $serializedArray = '{"expirationLifetime":' . $lifeTime . '}'; + + $this->persistentHelperMock->expects($this->once()) + ->method('getLifeTime') + ->willReturn($lifeTime); + $this->jsonSerializerMock->expects($this->once()) + ->method('serialize') + ->with($arrayToSerialize) + ->willReturn($serializedArray); + + $this->assertEquals($serializedArray, $this->additional->getConfig()); } } diff --git a/app/code/Magento/Persistent/Test/Unit/Model/Checkout/GuestPaymentInformationManagementPluginTest.php b/app/code/Magento/Persistent/Test/Unit/Model/Checkout/GuestPaymentInformationManagementPluginTest.php index b9285715146a5..c7f84b476fa7e 100644 --- a/app/code/Magento/Persistent/Test/Unit/Model/Checkout/GuestPaymentInformationManagementPluginTest.php +++ b/app/code/Magento/Persistent/Test/Unit/Model/Checkout/GuestPaymentInformationManagementPluginTest.php @@ -102,8 +102,7 @@ public function testBeforeSavePaymentInformationAndPlaceOrderCartConvertsToGuest ['setCustomerEmail', 'getAddressesCollection'], false ); - $this->checkoutSessionMock->expects($this->once())->method('getQuote')->willReturn($quoteMock); - $quoteMock->expects($this->once())->method('getId')->willReturn($cartId); + $this->checkoutSessionMock->method('getQuoteId')->willReturn($cartId); $this->cartRepositoryMock->expects($this->once())->method('get')->with($cartId)->willReturn($quoteMock); $quoteMock->expects($this->once())->method('setCustomerEmail')->with($email); /** @var \Magento\Framework\Data\Collection|\PHPUnit_Framework_MockObject_MockObject $collectionMock */ diff --git a/app/code/Magento/Persistent/Test/Unit/Model/ObserverTest.php b/app/code/Magento/Persistent/Test/Unit/Model/ObserverTest.php index 7008a9eb25e5d..dd299fe30d646 100644 --- a/app/code/Magento/Persistent/Test/Unit/Model/ObserverTest.php +++ b/app/code/Magento/Persistent/Test/Unit/Model/ObserverTest.php @@ -80,31 +80,18 @@ protected function setUp() ); } + /** + * @return void + */ public function testEmulateWelcomeBlock() { - $customerId = 1; - $customerName = 'Test Customer Name'; - $welcomeMessage = __('Welcome, %1!', $customerName); - $customerMock = $this->getMockForAbstractClass(\Magento\Customer\Api\Data\CustomerInterface::class); + $welcomeMessage = __(' '); $block = $this->getMockBuilder(\Magento\Framework\View\Element\AbstractBlock::class) ->disableOriginalConstructor() ->setMethods(['setWelcome']) ->getMock(); - $headerAdditionalBlock = $this->getMockBuilder(\Magento\Framework\View\Element\AbstractBlock::class) - ->disableOriginalConstructor() - ->getMock(); - $this->persistentSessionMock->expects($this->once())->method('getSession')->willReturn($this->sessionMock); - $this->sessionMock->expects($this->once())->method('getCustomerId')->willReturn($customerId); - $this->customerRepositoryMock - ->expects($this->once()) - ->method('getById') - ->with($customerId)->willReturn($customerMock); - $this->customerViewHelperMock->expects($this->once())->method('getCustomerName')->willReturn($customerName); - $this->layoutMock->expects($this->once()) - ->method('getBlock') - ->with('header.additional') - ->willReturn($headerAdditionalBlock); $block->expects($this->once())->method('setWelcome')->with($welcomeMessage); + $this->observer->emulateWelcomeBlock($block); } } diff --git a/app/code/Magento/Persistent/Test/Unit/Model/QuoteManagerTest.php b/app/code/Magento/Persistent/Test/Unit/Model/QuoteManagerTest.php index 7d3d1c8487627..86b906f3cb35e 100644 --- a/app/code/Magento/Persistent/Test/Unit/Model/QuoteManagerTest.php +++ b/app/code/Magento/Persistent/Test/Unit/Model/QuoteManagerTest.php @@ -247,8 +247,8 @@ public function testConvertCustomerCartToGuest() $emailArgs = ['email' => null]; $this->checkoutSessionMock->expects($this->once()) - ->method('getQuote')->willReturn($this->quoteMock); - $this->quoteMock->expects($this->exactly(2))->method('getId')->willReturn($quoteId); + ->method('getQuoteId')->willReturn($quoteId); + $this->quoteMock->expects($this->once())->method('getId')->willReturn($quoteId); $this->quoteRepositoryMock->expects($this->once())->method('get')->with($quoteId)->willReturn($this->quoteMock); $this->quoteMock->expects($this->once()) ->method('setIsActive')->with(true)->willReturn($this->quoteMock); @@ -288,18 +288,15 @@ public function testConvertCustomerCartToGuest() public function testConvertCustomerCartToGuestWithEmptyQuote() { $this->checkoutSessionMock->expects($this->once()) - ->method('getQuote')->willReturn($this->quoteMock); - $this->quoteMock->expects($this->once())->method('getId')->willReturn(null); + ->method('getQuoteId')->willReturn(null); $this->quoteRepositoryMock->expects($this->once())->method('get')->with(null)->willReturn(null); - $this->model->convertCustomerCartToGuest(); } public function testConvertCustomerCartToGuestWithEmptyQuoteId() { $this->checkoutSessionMock->expects($this->once()) - ->method('getQuote')->willReturn($this->quoteMock); - $this->quoteMock->expects($this->once())->method('getId')->willReturn(1); + ->method('getQuoteId')->willReturn(1); $quoteWithNoId = $this->quoteMock = $this->createMock(\Magento\Quote\Model\Quote::class); $quoteWithNoId->expects($this->once())->method('getId')->willReturn(null); $this->quoteRepositoryMock->expects($this->once())->method('get')->with(1)->willReturn($quoteWithNoId); diff --git a/app/code/Magento/Persistent/etc/di.xml b/app/code/Magento/Persistent/etc/di.xml index f49d4361acb52..c28426b4f25bf 100644 --- a/app/code/Magento/Persistent/etc/di.xml +++ b/app/code/Magento/Persistent/etc/di.xml @@ -12,4 +12,7 @@ <type name="Magento\Customer\CustomerData\Customer"> <plugin name="section_data" type="Magento\Persistent\Model\Plugin\CustomerData" /> </type> + <type name="Magento\Framework\App\Http\Context"> + <plugin name="persistent_page_cache_variation" type="Magento\Persistent\Model\Plugin\PersistentCustomerContext" /> + </type> </config> diff --git a/app/code/Magento/Persistent/etc/frontend/di.xml b/app/code/Magento/Persistent/etc/frontend/di.xml index f976f4de79c21..3c33f8a51c418 100644 --- a/app/code/Magento/Persistent/etc/frontend/di.xml +++ b/app/code/Magento/Persistent/etc/frontend/di.xml @@ -35,4 +35,18 @@ <type name="Magento\Checkout\Model\GuestPaymentInformationManagement"> <plugin name="inject_guest_address_for_nologin" type="Magento\Persistent\Model\Checkout\GuestPaymentInformationManagementPlugin" /> </type> + <type name="Magento\Customer\CustomerData\SectionPoolInterface"> + <arguments> + <argument name="sectionSourceMap" xsi:type="array"> + <item name="persistent" xsi:type="string">Magento\Persistent\CustomerData\Persistent</item> + </argument> + </arguments> + </type> + <type name="Magento\Customer\Block\CustomerData"> + <arguments> + <argument name="expirableSectionNames" xsi:type="array"> + <item name="persistent" xsi:type="string">persistent</item> + </argument> + </arguments> + </type> </config> diff --git a/app/code/Magento/Persistent/etc/frontend/sections.xml b/app/code/Magento/Persistent/etc/frontend/sections.xml new file mode 100644 index 0000000000000..16b44c502fc47 --- /dev/null +++ b/app/code/Magento/Persistent/etc/frontend/sections.xml @@ -0,0 +1,13 @@ +<?xml version="1.0"?> +<!-- +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:module:Magento_Customer:etc/sections.xsd"> + <action name="persistent/index/unsetCookie"> + <section name="persistent"/> + </action> +</config> diff --git a/app/code/Magento/Persistent/view/frontend/requirejs-config.js b/app/code/Magento/Persistent/view/frontend/requirejs-config.js new file mode 100644 index 0000000000000..e30e07c454be5 --- /dev/null +++ b/app/code/Magento/Persistent/view/frontend/requirejs-config.js @@ -0,0 +1,14 @@ +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +var config = { + config: { + mixins: { + 'Magento_Customer/js/customer-data': { + 'Magento_Persistent/js/view/customer-data-mixin': true + } + } + } +}; diff --git a/app/code/Magento/Persistent/view/frontend/templates/additional.phtml b/app/code/Magento/Persistent/view/frontend/templates/additional.phtml new file mode 100644 index 0000000000000..28dce5dc23cc9 --- /dev/null +++ b/app/code/Magento/Persistent/view/frontend/templates/additional.phtml @@ -0,0 +1,21 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +?> +<?php if ($block->getCustomerId()) :?> + <span> + <a <?= /* @escapeNotVerified */ $block->getLinkAttributes()?>><?= $block->escapeHtml(__('Not you?'));?></a> + </span> +<?php endif;?> +<script type="application/javascript"> + window.persistent = <?= /* @noEscape */ $block->getConfig(); ?>; +</script> +<script type="text/x-magento-init"> + { + "li.greet.welcome > span.not-logged-in": { + "Magento_Persistent/js/view/additional-welcome": {} + } + } +</script> diff --git a/app/code/Magento/Persistent/view/frontend/web/js/view/additional-welcome.js b/app/code/Magento/Persistent/view/frontend/web/js/view/additional-welcome.js new file mode 100644 index 0000000000000..47949671fed52 --- /dev/null +++ b/app/code/Magento/Persistent/view/frontend/web/js/view/additional-welcome.js @@ -0,0 +1,55 @@ +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +define([ + 'jquery', + 'mage/translate', + 'Magento_Customer/js/customer-data' +], function ($, $t, customerData) { + 'use strict'; + + return { + /** + * Init + */ + init: function () { + var persistent = customerData.get('persistent'); + + if (persistent().fullname === undefined) { + customerData.get('persistent').subscribe(this.replacePersistentWelcome); + } else { + this.replacePersistentWelcome(); + } + }, + + /** + * Replace welcome message for customer with persistent cookie. + */ + replacePersistentWelcome: function () { + var persistent = customerData.get('persistent'), + welcomeElems; + + if (persistent().fullname !== undefined) { + welcomeElems = $('li.greet.welcome > span.not-logged-in'); + + if (welcomeElems.length) { + $(welcomeElems).each(function () { + var html = $t('Welcome, %1!').replace('%1', persistent().fullname); + + $(this).attr('data-bind', html); + $(this).html(html); + }); + } + } + }, + + /** + * @constructor + */ + 'Magento_Persistent/js/view/additional-welcome': function () { + this.init(); + } + }; +}); diff --git a/app/code/Magento/Persistent/view/frontend/web/js/view/customer-data-mixin.js b/app/code/Magento/Persistent/view/frontend/web/js/view/customer-data-mixin.js new file mode 100644 index 0000000000000..855404c6f6f32 --- /dev/null +++ b/app/code/Magento/Persistent/view/frontend/web/js/view/customer-data-mixin.js @@ -0,0 +1,51 @@ +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +define([ + 'jquery', + 'mage/utils/wrapper' +], function ($, wrapper) { + 'use strict'; + + var mixin = { + + /** + * Check if persistent section is expired due to lifetime. + * + * @param {Function} originFn - Original method. + * @return {Array} + */ + getExpiredSectionNames: function (originFn) { + var expiredSections = originFn(), + storage = $.initNamespaceStorage('mage-cache-storage').localStorage, + currentTimestamp = Math.floor(Date.now() / 1000), + persistentIndex = expiredSections.indexOf('persistent'), + persistentLifeTime = 0, + sectionData; + + if (window.persistent !== undefined && window.persistent.expirationLifetime !== undefined) { + persistentLifeTime = window.persistent.expirationLifetime; + } + + if (persistentIndex !== -1) { + sectionData = storage.get('persistent'); + + if (typeof sectionData === 'object' && + sectionData['data_id'] + persistentLifeTime >= currentTimestamp + ) { + expiredSections.splice(persistentIndex, 1); + } + } + + return expiredSections; + } + }; + + /** + * Override default customer-data.getExpiredSectionNames(). + */ + return function (target) { + return wrapper.extend(target, mixin); + }; +}); diff --git a/app/code/Magento/ProductVideo/Model/Plugin/Catalog/Product/Gallery/CreateHandler.php b/app/code/Magento/ProductVideo/Model/Plugin/Catalog/Product/Gallery/CreateHandler.php index c56e5e3139517..d554b5dd68db2 100644 --- a/app/code/Magento/ProductVideo/Model/Plugin/Catalog/Product/Gallery/CreateHandler.php +++ b/app/code/Magento/ProductVideo/Model/Plugin/Catalog/Product/Gallery/CreateHandler.php @@ -20,6 +20,8 @@ class CreateHandler extends AbstractHandler const ADDITIONAL_STORE_DATA_KEY = 'additional_store_data'; /** + * Execute before Plugin + * * @param \Magento\Catalog\Model\Product\Gallery\CreateHandler $mediaGalleryCreateHandler * @param \Magento\Catalog\Model\Product $product * @param array $arguments @@ -45,6 +47,8 @@ public function beforeExecute( } /** + * Execute plugin + * * @param \Magento\Catalog\Model\Product\Gallery\CreateHandler $mediaGalleryCreateHandler * @param \Magento\Catalog\Model\Product $product * @return \Magento\Catalog\Model\Product @@ -59,6 +63,9 @@ public function afterExecute( ); if (!empty($mediaCollection)) { + if ($product->getIsDuplicate() === true) { + $mediaCollection = $this->makeAllNewVideos($product->getId(), $mediaCollection); + } $newVideoCollection = $this->collectNewVideos($mediaCollection); $this->saveVideoData($newVideoCollection, 0); @@ -71,6 +78,8 @@ public function afterExecute( } /** + * Saves video data + * * @param array $videoDataCollection * @param int $storeId * @return void @@ -84,6 +93,8 @@ protected function saveVideoData(array $videoDataCollection, $storeId) } /** + * Saves additioanal video data + * * @param array $videoDataCollection * @return void */ @@ -100,6 +111,8 @@ protected function saveAdditionalStoreData(array $videoDataCollection) } /** + * Saves video data + * * @param array $item * @return void */ @@ -112,6 +125,8 @@ protected function saveVideoValuesItem(array $item) } /** + * Excludes current store data + * * @param array $mediaCollection * @param int $currentStoreId * @return array @@ -127,6 +142,8 @@ function ($item) use ($currentStoreId) { } /** + * Prepare video data for saving + * * @param array $rowData * @return array */ @@ -144,6 +161,8 @@ protected function prepareVideoRowDataForSave(array $rowData) } /** + * Loads video data + * * @param array $mediaCollection * @param int $excludedStore * @return array @@ -166,6 +185,8 @@ protected function loadStoreViewVideoData(array $mediaCollection, $excludedStore } /** + * Collect video data + * * @param array $mediaCollection * @return array */ @@ -183,6 +204,8 @@ protected function collectVideoData(array $mediaCollection) } /** + * Extract video data + * * @param array $rowData * @return array */ @@ -195,6 +218,8 @@ protected function extractVideoDataFromRowData(array $rowData) } /** + * Collect items for additional data adding + * * @param array $mediaCollection * @return array */ @@ -210,6 +235,8 @@ protected function collectVideoEntriesIdsToAdditionalLoad(array $mediaCollection } /** + * Add additional data + * * @param array $mediaCollection * @param array $data * @return array @@ -230,6 +257,8 @@ protected function addAdditionalStoreData(array $mediaCollection, array $data): } /** + * Creates additional video data + * * @param array $storeData * @param int $valueId * @return array @@ -248,6 +277,8 @@ protected function createAdditionalStoreDataCollection(array $storeData, $valueI } /** + * Collect new videos + * * @param array $mediaCollection * @return array */ @@ -263,6 +294,8 @@ private function collectNewVideos(array $mediaCollection): array } /** + * Checks if gallery item is video + * * @param $item * @return bool */ @@ -274,6 +307,8 @@ private function isVideoItem($item): bool } /** + * Checks if video is new + * * @param $item * @return bool */ @@ -283,4 +318,23 @@ private function isNewVideo($item): bool || empty($item['video_url_default']) || empty($item['video_title_default']); } + + /** + * Mark all videos as new + * + * @param int $entityId + * @param array $mediaCollection + * @return array + */ + private function makeAllNewVideos($entityId, array $mediaCollection): array + { + foreach ($mediaCollection as $key => $video) { + if ($this->isVideoItem($video)) { + unset($video['video_url_default'], $video['video_title_default']); + $video['entity_id'] = $entityId; + $mediaCollection[$key] = $video; + } + } + return $mediaCollection; + } } diff --git a/app/code/Magento/ProductVideo/Test/Mftf/ActionGroup/AdminProductVideoActionGroup.xml b/app/code/Magento/ProductVideo/Test/Mftf/ActionGroup/AdminProductVideoActionGroup.xml new file mode 100644 index 0000000000000..621caea0cfc6d --- /dev/null +++ b/app/code/Magento/ProductVideo/Test/Mftf/ActionGroup/AdminProductVideoActionGroup.xml @@ -0,0 +1,46 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> + <!-- Add video in Admin Product page --> + <actionGroup name="addProductVideo"> + <arguments> + <argument name="video" defaultValue="mftfTestProductVideo"/> + </arguments> + + <scrollTo selector="{{AdminProductImagesSection.productImagesToggle}}" x="0" y="-100" stepKey="scrollToArea"/> + <conditionalClick selector="{{AdminProductImagesSection.productImagesToggle}}" + dependentSelector="{{AdminProductImagesSection.imageUploadButton}}" + visible="false" + stepKey="openProductVideoSection"/> + <waitForElementVisible selector="{{AdminProductImagesSection.addVideoButton}}" stepKey="waitForAddVideoButtonVisible"/> + <click selector="{{AdminProductImagesSection.addVideoButton}}" stepKey="addVideo"/> + <waitForElementVisible selector="{{AdminProductNewVideoSection.videoUrlTextField}}" stepKey="waitForUrlElementVisible"/> + <fillField selector="{{AdminProductNewVideoSection.videoUrlTextField}}" userInput="{{video.videoUrl}}" stepKey="fillFieldVideoUrl"/> + <fillField selector="{{AdminProductNewVideoSection.videoTitleTextField}}" userInput="{{video.videoTitle}}" stepKey="fillFieldVideoTitle"/> + <waitForPageLoad stepKey="waitForPageLoad"/> + <waitForElementNotVisible selector="{{AdminProductNewVideoSection.saveButtonDisabled}}" wait="30" stepKey="waitForSaveButtonVisible"/> + <click selector="{{AdminProductNewVideoSection.saveButton}}" stepKey="saveVideo"/> + <waitForLoadingMaskToDisappear stepKey="waitForLoadingMaskToDisappear"/> + </actionGroup> + + <!-- Assert product video in Admin Product page --> + <actionGroup name="assertProductVideoAdminProductPage"> + <arguments> + <argument name="video" defaultValue="mftfTestProductVideo"/> + </arguments> + <scrollTo selector="{{AdminProductImagesSection.productImagesToggle}}" x="0" y="-100" stepKey="scrollToArea"/> + <conditionalClick selector="{{AdminProductImagesSection.productImagesToggle}}" + dependentSelector="{{AdminProductImagesSection.imageUploadButton}}" + visible="false" + stepKey="openProductVideoSection"/> + <waitForPageLoad stepKey="waitForPageLoad"/> + <seeElement selector="{{AdminProductImagesSection.videoTitleText(video.videoShortTitle)}}" stepKey="seeVideoTitle"/> + <seeElementInDOM selector="{{AdminProductImagesSection.videoUrlHiddenField(video.videoUrl)}}" stepKey="seeVideoItem"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/ProductVideo/Test/Mftf/ActionGroup/StorefrontProductVideoActionGroup.xml b/app/code/Magento/ProductVideo/Test/Mftf/ActionGroup/StorefrontProductVideoActionGroup.xml new file mode 100644 index 0000000000000..28634f41deec1 --- /dev/null +++ b/app/code/Magento/ProductVideo/Test/Mftf/ActionGroup/StorefrontProductVideoActionGroup.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"> + <!--Assert product video in Storefront Product page --> + <actionGroup name="assertProductVideoStorefrontProductPage"> + <arguments> + <argument name="dataTypeAttribute" defaultValue="'youtube'"/> + </arguments> + <seeElement selector="{{StorefrontProductInfoMainSection.productVideo(dataTypeAttribute)}}" stepKey="seeProductVideoDataType"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/ProductVideo/Test/Mftf/Data/ProductVideoConfigData.xml b/app/code/Magento/ProductVideo/Test/Mftf/Data/ProductVideoConfigData.xml new file mode 100644 index 0000000000000..8fe5899e91ef8 --- /dev/null +++ b/app/code/Magento/ProductVideo/Test/Mftf/Data/ProductVideoConfigData.xml @@ -0,0 +1,26 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<entities xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:DataGenerator/etc/dataProfileSchema.xsd"> + <!-- mftf test youtube api key configuration --> + <entity name="ProductVideoYoutubeApiKeyConfig" type="product_video_config"> + <requiredEntity type="youtube_api_key_config">YouTubeApiKey</requiredEntity> + </entity> + <entity name="YouTubeApiKey" type="youtube_api_key_config"> + <data key="value">AIzaSyDwqDWuw1lra-LnpJL2Mr02DYuFmkuRSns</data> + </entity> + + <!-- default configuration used to restore Magento config --> + <entity name="DefaultProductVideoConfig" type="product_video_config"> + <requiredEntity type="youtube_api_key_config">DefaultYouTubeApiKey</requiredEntity> + </entity> + <entity name="DefaultYouTubeApiKey" type="youtube_api_key_config"> + <data key="value"/> + </entity> +</entities> diff --git a/app/code/Magento/ProductVideo/Test/Mftf/Data/ProductVideoData.xml b/app/code/Magento/ProductVideo/Test/Mftf/Data/ProductVideoData.xml new file mode 100644 index 0000000000000..5bc4ad86e0f06 --- /dev/null +++ b/app/code/Magento/ProductVideo/Test/Mftf/Data/ProductVideoData.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="mftfTestProductVideo" type="product_video"> + <data key="videoUrl">https://youtu.be/bpOSxM0rNPM</data> + <data key="videoTitle">Arctic Monkeys - Do I Wanna Know? (Official Video)</data> + <data key="videoShortTitle">Arctic Monkeys</data> + </entity> +</entities> diff --git a/app/code/Magento/ProductVideo/Test/Mftf/Metadata/product_video_config-meta.xml b/app/code/Magento/ProductVideo/Test/Mftf/Metadata/product_video_config-meta.xml new file mode 100644 index 0000000000000..dc6d3af1c52c5 --- /dev/null +++ b/app/code/Magento/ProductVideo/Test/Mftf/Metadata/product_video_config-meta.xml @@ -0,0 +1,21 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<operations xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:DataGenerator/etc/dataOperation.xsd"> + <operation name="CreateProductVideoYouTubeApiKeyConfig" dataType="product_video_config" type="create" auth="adminFormKey" url="admin/system_config/save/section/catalog/" method="POST" successRegex="/messages-message-success/"> + <object key="groups" dataType="product_video_config"> + <object key="product_video" dataType="product_video_config"> + <object key="fields" dataType="product_video_config"> + <object key="youtube_api_key" dataType="youtube_api_key_config"> + <field key="value">string</field> + </object> + </object> + </object> + </object> + </operation> +</operations> diff --git a/app/code/Magento/ProductVideo/Test/Mftf/Page/AdminProductCreatePage.xml b/app/code/Magento/ProductVideo/Test/Mftf/Page/AdminProductCreatePage.xml new file mode 100644 index 0000000000000..52f13a1da5188 --- /dev/null +++ b/app/code/Magento/ProductVideo/Test/Mftf/Page/AdminProductCreatePage.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="AdminProductCreatePage" url="catalog/product/new/set/{{set}}/type/{{type}}/" area="admin" module="Magento_Catalog" parameterized="true"> + <section name="AdminProductNewVideoSection"/> + </page> +</pages> diff --git a/app/code/Magento/ProductVideo/Test/Mftf/Section/AdminProductImagesSection.xml b/app/code/Magento/ProductVideo/Test/Mftf/Section/AdminProductImagesSection.xml new file mode 100644 index 0000000000000..913ae8c955340 --- /dev/null +++ b/app/code/Magento/ProductVideo/Test/Mftf/Section/AdminProductImagesSection.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="AdminProductImagesSection"> + <element name="addVideoButton" type="button" selector="#add_video_button" timeout="60"/> + <element name="videoUrlHiddenField" type="text" selector="#media_gallery_content input[value*='{{title}}']" parameterized="true"/> + <element name="videoTitleText" type="text" selector="//*[@id='media_gallery_content']//div[contains(text(), '{{title}}')]" parameterized="true"/> + </section> +</sections> diff --git a/app/code/Magento/ProductVideo/Test/Mftf/Section/AdminProductNewVideoSection.xml b/app/code/Magento/ProductVideo/Test/Mftf/Section/AdminProductNewVideoSection.xml new file mode 100644 index 0000000000000..8df254aea7c50 --- /dev/null +++ b/app/code/Magento/ProductVideo/Test/Mftf/Section/AdminProductNewVideoSection.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="AdminProductNewVideoSection"> + <element name="saveButton" type="button" selector=".action-primary.video-create-button" timeout="30"/> + <element name="saveButtonDisabled" type="text" selector="button.action-primary.video-create-button[disabled='disabled']"/> + <element name="videoUrlTextField" type="input" selector="#video_url"/> + <element name="videoTitleTextField" type="input" selector="#video_title"/> + </section> +</sections> diff --git a/app/code/Magento/ProductVideo/Test/Mftf/Section/StorefrontProductInfoMainSection.xml b/app/code/Magento/ProductVideo/Test/Mftf/Section/StorefrontProductInfoMainSection.xml new file mode 100644 index 0000000000000..1ce56ff4996ab --- /dev/null +++ b/app/code/Magento/ProductVideo/Test/Mftf/Section/StorefrontProductInfoMainSection.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="StorefrontProductInfoMainSection"> + <element name="productVideo" type="text" selector=".product-video[data-type='{{videoType}}']" parameterized="true"/> + <element name="clickInVideo" type="video" selector=".fotorama__stage__shaft"/> + <element name="videoPausedMode" type="video" selector="[class*='paused-mode']"/> + <element name="videoPlayedMode" type="video" selector="[class*='playing-mode']"/> + <element name="frameVideo" type="video" selector="widget2"/> + </section> +</sections> diff --git a/app/code/Magento/ProductVideo/Test/Mftf/Test/StorefrontYoutubeVideoWindowOnProductPageTest.xml b/app/code/Magento/ProductVideo/Test/Mftf/Test/StorefrontYoutubeVideoWindowOnProductPageTest.xml new file mode 100644 index 0000000000000..15888d1af8ee5 --- /dev/null +++ b/app/code/Magento/ProductVideo/Test/Mftf/Test/StorefrontYoutubeVideoWindowOnProductPageTest.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="StorefrontYoutubeVideoWindowOnProductPageTest"> + <annotations> + <features value="ProductVideo"/> + <stories value="MAGETWO-87734: [Sigma Beauty]Cannot pause Youtube video in IE 11"/> + <testCaseId value="MAGETWO-96677"/> + <title value="Youtube video window on the product page"/> + <description value="Check Youtube video window on the product page"/> + <severity value="MAJOR"/> + <group value="productVideo"/> + </annotations> + + <before> + <!--Log In--> + <actionGroup ref="LoginAsAdmin" stepKey="loginAsAdmin"/> + <!--Create category--> + <createData entity="_defaultCategory" stepKey="createCategory"/> + <!--Create product--> + <createData entity="SimpleProduct" stepKey="createProduct"> + <requiredEntity createDataKey="createCategory"/> + </createData> + <!-- Set product video Youtube api key configuration --> + <createData entity="ProductVideoYoutubeApiKeyConfig" stepKey="setStoreConfig"/> + </before> + + <after> + <!--Clear all filters on products grid page--> + <amOnPage url="{{AdminProductIndexPage.url}}" stepKey="goToProductsIndex"/> + <actionGroup ref="clearFiltersAdminDataGrid" stepKey="clearExistingProductFilters"/> + <!--Delete created product and category--> + <deleteData createDataKey="createProduct" stepKey="deleteSimpleProduct"/> + <deleteData createDataKey="createCategory" stepKey="deleteCategoryFirst"/> + <!-- Set product video configuration to default --> + <createData entity="DefaultProductVideoConfig" stepKey="setStoreDefaultConfig"/> + <!--Log Out--> + <actionGroup ref="logout" stepKey="logout"/> + </after> + + <!--Open simple product--> + <amOnPage url="{{AdminProductIndexPage.url}}" stepKey="goToProductIndex"/> + <actionGroup ref="AdminGridFilterSearchResultsByInput" stepKey="filterProductGridBySku"> + <argument name="inputName" value="sku"/> + <argument name="value" value="$$createProduct.sku$$"/> + </actionGroup> + <click selector="{{AdminDataGridTableSection.firstRow}}" stepKey="openFirstProductForEdit"/> + <waitForPageLoad stepKey="waitProductPageIsLoaded"/> + + <!-- Add product video --> + <actionGroup ref="addProductVideo" stepKey="addProductVideo"/> + <!-- Assert product video in admin product form --> + <actionGroup ref="assertProductVideoAdminProductPage" stepKey="assertProductVideoAdminProductPage"/> + + <!-- Save the product --> + <click selector="{{AdminProductFormActionSection.saveButton}}" stepKey="saveProduct"/> + <waitForPageLoad stepKey="waitForProductSaved"/> + + <!-- Assert product video in storefront product page --> + <amOnPage url="{{StorefrontProductPage.url($$createProduct.name$$)}}" stepKey="goToStorefrontProductPage"/> + <actionGroup ref="assertProductVideoStorefrontProductPage" stepKey="assertProductVideoStorefrontProductPage"/> + + <!--Click Play video button--> + <click selector="{{StorefrontProductInfoMainSection.clickInVideo}}" stepKey="clickToPlayVideo"/> + <wait time="5" stepKey="waitFiveSecondToPlayVideo"/> + <switchToIFrame selector="{{StorefrontProductInfoMainSection.frameVideo}}" stepKey="switchToFrame"/> + <waitForElementVisible selector="{{StorefrontProductInfoMainSection.videoPlayedMode}}" stepKey="waitForVideoPlayed"/> + <seeElement selector="{{StorefrontProductInfoMainSection.videoPlayedMode}}" stepKey="assertVideoIsPlayed"/> + <switchToIFrame stepKey="switchBack"/> + + <!--Click Pause button--> + <click selector="{{StorefrontProductInfoMainSection.clickInVideo}}" stepKey="clickToStopVideo"/> + <wait time="5" stepKey="waitFiveSecondToStopVideo"/> + <switchToIFrame selector="{{StorefrontProductInfoMainSection.frameVideo}}" stepKey="switchToFrame1"/> + <waitForElementVisible selector="{{StorefrontProductInfoMainSection.videoPausedMode}}" stepKey="waitForVideoPaused"/> + <seeElement selector="{{StorefrontProductInfoMainSection.videoPausedMode}}" stepKey="assertVideoIsPaused"/> + <switchToIFrame stepKey="switchBack1"/> + + <!--Click Play video button again. Make sure that Video continued playing--> + <click selector="{{StorefrontProductInfoMainSection.clickInVideo}}" stepKey="clickAgainToPlayVideo"/> + <wait time="5" stepKey="waitAgainFiveSecondToPlayVideo"/> + <switchToIFrame selector="{{StorefrontProductInfoMainSection.frameVideo}}" stepKey="switchToFrame2"/> + <waitForElementVisible selector="{{StorefrontProductInfoMainSection.videoPlayedMode}}" stepKey="waitForVideoPlayedAgain"/> + <seeElement selector="{{StorefrontProductInfoMainSection.videoPlayedMode}}" stepKey="assertVideoIsPlayedAgain"/> + <switchToIFrame stepKey="switchBack2"/> + </test> +</tests> + diff --git a/app/code/Magento/Quote/Api/Data/CartInterface.php b/app/code/Magento/Quote/Api/Data/CartInterface.php index 551833e3effb1..b87869de6b3df 100644 --- a/app/code/Magento/Quote/Api/Data/CartInterface.php +++ b/app/code/Magento/Quote/Api/Data/CartInterface.php @@ -223,14 +223,14 @@ public function setBillingAddress(\Magento\Quote\Api\Data\AddressInterface $bill /** * Returns the reserved order ID for the cart. * - * @return int|null Reserved order ID. Otherwise, null. + * @return string|null Reserved order ID. Otherwise, null. */ public function getReservedOrderId(); /** * Sets the reserved order ID for the cart. * - * @param int $reservedOrderId + * @param string $reservedOrderId * @return $this */ public function setReservedOrderId($reservedOrderId); diff --git a/app/code/Magento/Quote/Model/Quote/Address/Total.php b/app/code/Magento/Quote/Model/Quote/Address/Total.php index 42224c970ed27..00060c15c10d8 100644 --- a/app/code/Magento/Quote/Model/Quote/Address/Total.php +++ b/app/code/Magento/Quote/Model/Quote/Address/Total.php @@ -6,6 +6,8 @@ namespace Magento\Quote\Model\Quote\Address; /** + * Class Total + * * @method string getCode() * * @api @@ -54,6 +56,8 @@ public function __construct( */ public function setTotalAmount($code, $amount) { + $amount = is_float($amount) ? round($amount, 4) : $amount; + $this->totalAmounts[$code] = $amount; if ($code != 'subtotal') { $code = $code . '_amount'; @@ -72,6 +76,8 @@ public function setTotalAmount($code, $amount) */ public function setBaseTotalAmount($code, $amount) { + $amount = is_float($amount) ? round($amount, 4) : $amount; + $this->baseTotalAmounts[$code] = $amount; if ($code != 'subtotal') { $code = $code . '_amount'; @@ -167,6 +173,7 @@ public function getAllBaseTotalAmounts() /** * Set the full info, which is used to capture tax related information. + * * If a string is used, it is assumed to be serialized. * * @param array|string $info diff --git a/app/code/Magento/Quote/Model/Quote/Address/Total/Shipping.php b/app/code/Magento/Quote/Model/Quote/Address/Total/Shipping.php index 84f1fc1c35adf..e9a63dad6e169 100644 --- a/app/code/Magento/Quote/Model/Quote/Address/Total/Shipping.php +++ b/app/code/Magento/Quote/Model/Quote/Address/Total/Shipping.php @@ -9,6 +9,9 @@ use Magento\Quote\Api\Data\AddressInterface; use Magento\Quote\Model\Quote\Address\FreeShippingInterface; +/** + * Collect totals for shipping. + */ class Shipping extends \Magento\Quote\Model\Quote\Address\Total\AbstractTotal { /** @@ -111,7 +114,7 @@ public function fetch(\Magento\Quote\Model\Quote $quote, \Magento\Quote\Model\Qu { $amount = $total->getShippingAmount(); $shippingDescription = $total->getShippingDescription(); - $title = ($amount != 0 && $shippingDescription) + $title = ($shippingDescription) ? __('Shipping & Handling (%1)', $shippingDescription) : __('Shipping & Handling'); @@ -227,7 +230,7 @@ private function getAssignmentWeightData(AddressInterface $address, array $items * @param bool $addressFreeShipping * @param float $itemWeight * @param float $itemQty - * @param $freeShipping + * @param bool $freeShipping * @return float */ private function getItemRowWeight( diff --git a/app/code/Magento/Quote/Model/Quote/Item/Updater.php b/app/code/Magento/Quote/Model/Quote/Item/Updater.php index 6a7a3c1c1839e..05244d4ecc43a 100644 --- a/app/code/Magento/Quote/Model/Quote/Item/Updater.php +++ b/app/code/Magento/Quote/Model/Quote/Item/Updater.php @@ -145,8 +145,8 @@ protected function unsetCustomPrice(Item $item) $item->addOption($infoBuyRequest); } - $item->unsetData('custom_price'); - $item->unsetData('original_custom_price'); + $item->setData('custom_price', null); + $item->setData('original_custom_price', null); } /** diff --git a/app/code/Magento/Quote/Test/Unit/Model/Quote/Item/UpdaterTest.php b/app/code/Magento/Quote/Test/Unit/Model/Quote/Item/UpdaterTest.php index 7933da7c5fe37..8e6a3723caa7c 100644 --- a/app/code/Magento/Quote/Test/Unit/Model/Quote/Item/UpdaterTest.php +++ b/app/code/Magento/Quote/Test/Unit/Model/Quote/Item/UpdaterTest.php @@ -67,7 +67,7 @@ protected function setUp() 'addOption', 'setCustomPrice', 'setOriginalCustomPrice', - 'unsetData', + 'setData', 'hasData', 'setIsQtyDecimal' ]); @@ -301,7 +301,7 @@ public function testUpdateUnsetCustomPrice() 'setProduct', 'getData', 'unsetData', - 'hasData' + 'hasData', ]); $buyRequestMock->expects($this->never())->method('setCustomPrice'); $buyRequestMock->expects($this->once())->method('getData')->will($this->returnValue([])); @@ -353,7 +353,11 @@ public function testUpdateUnsetCustomPrice() ->will($this->returnValue($buyRequestMock)); $this->itemMock->expects($this->exactly(2)) - ->method('unsetData'); + ->method('setData') + ->withConsecutive( + ['custom_price', null], + ['original_custom_price', null] + ); $this->itemMock->expects($this->once()) ->method('hasData') diff --git a/app/code/Magento/Reports/Block/Adminhtml/Sales/Tax/Grid.php b/app/code/Magento/Reports/Block/Adminhtml/Sales/Tax/Grid.php index 79deb27423be5..7ca6b803f5b2b 100644 --- a/app/code/Magento/Reports/Block/Adminhtml/Sales/Tax/Grid.php +++ b/app/code/Magento/Reports/Block/Adminhtml/Sales/Tax/Grid.php @@ -123,7 +123,6 @@ protected function _prepareColumns() [ 'header' => __('Orders'), 'index' => 'orders_count', - 'total' => 'sum', 'type' => 'number', 'sortable' => false, 'header_css_class' => 'col-qty', diff --git a/app/code/Magento/Reports/Model/ResourceModel/Order/Collection.php b/app/code/Magento/Reports/Model/ResourceModel/Order/Collection.php index 82ebc74a0468e..fd9adbe734101 100644 --- a/app/code/Magento/Reports/Model/ResourceModel/Order/Collection.php +++ b/app/code/Magento/Reports/Model/ResourceModel/Order/Collection.php @@ -3,7 +3,6 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ - namespace Magento\Reports\Model\ResourceModel\Order; use Magento\Framework\DB\Select; @@ -81,7 +80,7 @@ class Collection extends \Magento\Sales\Model\ResourceModel\Order\Collection * @param \Magento\Framework\Stdlib\DateTime\TimezoneInterface $localeDate * @param \Magento\Sales\Model\Order\Config $orderConfig * @param \Magento\Sales\Model\ResourceModel\Report\OrderFactory $reportOrderFactory - * @param null $connection + * @param \Magento\Framework\DB\Adapter\AdapterInterface $connection * @param \Magento\Framework\Model\ResourceModel\Db\AbstractDb $resource * * @SuppressWarnings(PHPMD.ExcessiveParameterList) @@ -825,7 +824,7 @@ protected function getTotalsExpression( ) { $template = ($storeId != 0) ? '(main_table.base_subtotal - %2$s - %1$s - ABS(main_table.base_discount_amount) - %3$s)' - : '((main_table.base_subtotal - %1$s - %2$s - ABS(main_table.base_discount_amount) - %3$s) ' + : '((main_table.base_subtotal - %1$s - %2$s - ABS(main_table.base_discount_amount) + %3$s) ' . ' * main_table.base_to_global_rate)'; return sprintf($template, $baseSubtotalRefunded, $baseSubtotalCanceled, $baseDiscountCanceled); } diff --git a/app/code/Magento/Reports/Test/Mftf/Page/AdminReportSalesTaxPage.xml b/app/code/Magento/Reports/Test/Mftf/Page/AdminReportSalesTaxPage.xml new file mode 100644 index 0000000000000..ff90be1ced389 --- /dev/null +++ b/app/code/Magento/Reports/Test/Mftf/Page/AdminReportSalesTaxPage.xml @@ -0,0 +1,15 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<pages xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Page/etc/PageObject.xsd"> + <page name="AdminReportSalesTaxPage" url="reports/report_sales/tax/" module="Magento_Reports" area="admin"> + <section name="AdminReportMainActionSection"/> + <section name="AdminReportTaxSection"/> + </page> +</pages> diff --git a/app/code/Magento/Reports/Test/Mftf/Section/AdminReportMainActionSection.xml b/app/code/Magento/Reports/Test/Mftf/Section/AdminReportMainActionSection.xml new file mode 100644 index 0000000000000..207e508b6b020 --- /dev/null +++ b/app/code/Magento/Reports/Test/Mftf/Section/AdminReportMainActionSection.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="AdminReportMainActionSection"> + <element name="showReport" type="button" selector="#filter_form_submit" timeout="30"/> + </section> +</sections> diff --git a/app/code/Magento/Reports/Test/Mftf/Section/AdminReportTaxSection.xml b/app/code/Magento/Reports/Test/Mftf/Section/AdminReportTaxSection.xml new file mode 100644 index 0000000000000..a9583d8772688 --- /dev/null +++ b/app/code/Magento/Reports/Test/Mftf/Section/AdminReportTaxSection.xml @@ -0,0 +1,19 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<sections xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Page/etc/SectionObject.xsd"> + <section name="AdminReportTaxSection"> + <element name="refreshStatistics" type="button" selector="//*[@id='messages']//a[text()='here']" timeout="30"/> + <element name="dataPickerFrom" type="button" selector="#sales_report_from + button.ui-datepicker-trigger"/> + <element name="dataPickerTo" type="button" selector="#sales_report_to + button.ui-datepicker-trigger"/> + <element name="goTodayButton" type="button" selector="#ui-datepicker-div [data-handler='today']"/> + <element name="closeButton" type="button" selector="#ui-datepicker-div [data-handler='hide']"/> + <element name="row" type="button" selector="//td[contains(text(),'{{var1}}')]/..//td[last()]" parameterized="true"/> + </section> +</sections> diff --git a/app/code/Magento/Reports/Test/Mftf/Test/AdminTaxReportGridTest.xml b/app/code/Magento/Reports/Test/Mftf/Test/AdminTaxReportGridTest.xml new file mode 100644 index 0000000000000..073b4ce6fb90c --- /dev/null +++ b/app/code/Magento/Reports/Test/Mftf/Test/AdminTaxReportGridTest.xml @@ -0,0 +1,130 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> + <test name="AdminTaxReportGridTest"> + <annotations> + <stories value="MAGETWO-86649: Reports / Sales / Tax report show incorrect amount"/> + <title value="Checking Tax Report grid"/> + <description value="Checking Tax Report grid with tax rates and same zip code"/> + <severity value="MAJOR"/> + <testCaseId value="MAGETWO-97018"/> + <group value="tax_report"/> + <group value="reports"/> + </annotations> + <before> + <!--Log in as Admin--> + <actionGroup ref="LoginAsAdmin" stepKey="loginAsAdmin"/> + <!--Create Customer--> + <createData entity="Simple_US_Customer" stepKey="createCustomer"/> + <!--Create Product--> + <createData entity="SimpleSubCategory" stepKey="createCategory"/> + <createData entity="SimpleProduct" stepKey="createSimpleProduct"> + <requiredEntity createDataKey="createCategory"/> + </createData> + <!--Create product Tax Class and Tax Rates--> + <createData entity="ProductTaxClass" stepKey="createProductTaxClass"/> + <createData entity="TexasTaxRate" stepKey="createStateTaxRate"/> + <createData entity="AustinTaxRate" stepKey="createCityTaxRate"/> + <!--Get Data from product Tax Class--> + <getData entity="ProductTaxClassGetter" stepKey="getProductTaxClass"> + <requiredEntity createDataKey="createProductTaxClass"/> + </getData> + </before> + <after> + <!--Delete Tax Rules--> + <actionGroup ref="DeleteTaxRule" stepKey="deleteCityTaxRule"> + <argument name="taxRuleName" value="CityTaxRule"/> + </actionGroup> + <actionGroup ref="DeleteTaxRule" stepKey="deleteStateTaxRule"> + <argument name="taxRuleName" value="StateTaxRule"/> + </actionGroup> + <!--Delete Customer--> + <deleteData createDataKey="createCustomer" stepKey="deleteCustomer"/> + <!--Delete Product--> + <deleteData createDataKey="createCategory" stepKey="deleteCategory"/> + <deleteData createDataKey="createSimpleProduct" stepKey="deleteProduct"/> + <!--Delete Tax Rates and product Tax Class--> + <deleteData createDataKey="createStateTaxRate" stepKey="deleteTaxClass1"/> + <deleteData createDataKey="createCityTaxRate" stepKey="deleteTaxClass2"/> + <deleteData createDataKey="createProductTaxClass" stepKey="deleteProductTaxClass"/> + <actionGroup ref="logout" stepKey="logout"/> + </after> + + <!--Adding Tax Rule with all (*) zip code--> + <actionGroup ref="AddTaxRule" stepKey="addStateTaxRule"> + <argument name="taxRuleName" value="StateTaxRule"/> + <argument name="taxRate" value="$$createStateTaxRate$$"/> + <argument name="productTaxClass" value="$$getProductTaxClass$$"/> + </actionGroup> + <!--Adding Tax Rule with zip code--> + <actionGroup ref="AddTaxRule" stepKey="addCityTaxRule"> + <argument name="taxRuleName" value="CityTaxRule"/> + <argument name="taxRate" value="$$createCityTaxRate$$"/> + <argument name="productTaxClass" value="$$getProductTaxClass$$"/> + </actionGroup> + <!-- Open Product Edit --> + <amOnPage url="{{AdminProductEditPage.url($$createSimpleProduct.id$$)}}" stepKey="goToEditPage"/> + <selectOption selector="{{AdminProductFormSection.productTaxClass}}" userInput="$$getProductTaxClass.class_name$$" stepKey="selectCustomTaxClass"/> + <actionGroup ref="saveProductForm" stepKey="saveProduct"/> + <!--Create new order with existing Customer --> + <actionGroup ref="navigateToNewOrderPageExistingCustomer" stepKey="createNewOrder"> + <argument name="customer" value="$$createCustomer$$"/> + </actionGroup> + <!--Add product to order--> + <actionGroup ref="addSimpleProductToOrder" stepKey="addSimpleProductToOrder"> + <argument name="product" value="$$createSimpleProduct$$"/> + </actionGroup> + <!--Select shipping--> + <actionGroup ref="orderSelectFlatRateShipping" stepKey="selectFlatRateShipping"/> + <!--Select payment--> + <actionGroup ref="SelectCheckMoneyPaymentMethod" stepKey="selectCheckMoneyPayment"/> + <!--Submit order--> + <click selector="{{AdminOrderFormActionSection.submitOrder}}" stepKey="clickSubmitOrder"/> + <see selector="{{AdminMessagesSection.success}}" userInput="You created the order." stepKey="seeOrderSuccessMessage"/> + <!--Create and submit Invoice--> + <actionGroup ref="StartCreateInvoiceFromOrderPage" stepKey="createInvoice"/> + <actionGroup ref="SubmitInvoice" stepKey="submitInvoice"/> + <!--Create and submit Shipment --> + <actionGroup ref="StartCreateShipmentFromOrderPage" stepKey="createShipment"/> + <actionGroup ref="SubmitShipment" stepKey="submitShipment"/> + <!--Go to Report -> Tax --> + <amOnPage url="{{AdminReportSalesTaxPage.url}}" stepKey="goToReportTax"/> + <click selector="{{AdminReportTaxSection.refreshStatistics}}" stepKey="clickRefreshStatistics"/> + <see selector="{{AdminMessagesSection.success}}" userInput="Recent statistics have been updated." stepKey="seeReportSuccessMessage"/> + <!--Select Date From--> + <click selector="{{AdminReportTaxSection.dataPickerFrom}}" stepKey="clickOnDatePickerFrom"/> + <waitForElementVisible selector="{{AdminReportTaxSection.goTodayButton}}" stepKey="waitForGoTodayButton1"/> + <click selector="{{AdminReportTaxSection.goTodayButton}}" stepKey="selectToday1"/> + <click selector="{{AdminReportTaxSection.closeButton}}" stepKey="selectClose1"/> + <!--Select Date To--> + <click selector="{{AdminReportTaxSection.dataPickerTo}}" stepKey="clickOnDatePickerTo"/> + <waitForElementVisible selector="{{AdminReportTaxSection.goTodayButton}}" stepKey="waitForGoTodayButton2"/> + <click selector="{{AdminReportTaxSection.goTodayButton}}" stepKey="selectToday2"/> + <click selector="{{AdminReportTaxSection.closeButton}}" stepKey="selectClose2"/> + <click selector="{{AdminReportMainActionSection.showReport}}" stepKey="clickOnShowReportButton"/> + <!--Assert taxes--> + <grabTextFrom selector="{{AdminReportTaxSection.row($$createStateTaxRate.code$$)}}" stepKey="grabStateTax"/> + <assertEquals stepKey="checkStateTaxForProduct"> + <expectedResult type="string">$2.00</expectedResult> + <actualResult type="variable">$grabStateTax</actualResult> + </assertEquals> + <grabTextFrom selector="{{AdminReportTaxSection.row($$createCityTaxRate.code$$)}}" stepKey="grabCityTax"/> + <assertEquals stepKey="checkCityTaxForProduct"> + <expectedResult type="string">$10.00</expectedResult> + <actualResult type="variable">$grabCityTax</actualResult> + </assertEquals> + <!--Assert total--> + <grabTextFrom selector="{{AdminReportTaxSection.row('Subtotal')}}" stepKey="grabTotalTax"/> + <assertEquals stepKey="checkTotalTaxForProduct"> + <expectedResult type="string">$12.00</expectedResult> + <actualResult type="variable">$grabTotalTax</actualResult> + </assertEquals> + </test> +</tests> diff --git a/app/code/Magento/Review/Model/ResourceModel/Rating.php b/app/code/Magento/Review/Model/ResourceModel/Rating.php index 3f54c17f6ff7c..9cd9b0650a278 100644 --- a/app/code/Magento/Review/Model/ResourceModel/Rating.php +++ b/app/code/Magento/Review/Model/ResourceModel/Rating.php @@ -425,9 +425,11 @@ public function getReviewSummary($object, $onlyForCurrentStore = true) $data = $connection->fetchAll($select, [':review_id' => $object->getReviewId()]); + $currentStore = $this->_storeManager->isSingleStoreMode() ? $this->_storeManager->getStore()->getId() : null; + if ($onlyForCurrentStore) { foreach ($data as $row) { - if ($row['store_id'] == $this->_storeManager->getStore()->getId()) { + if ($row['store_id'] !== $currentStore) { $object->addData($row); } } diff --git a/app/code/Magento/Rule/Model/Condition/Sql/Builder.php b/app/code/Magento/Rule/Model/Condition/Sql/Builder.php index e2118445b6de7..51d79363cecfe 100644 --- a/app/code/Magento/Rule/Model/Condition/Sql/Builder.php +++ b/app/code/Magento/Rule/Model/Condition/Sql/Builder.php @@ -32,9 +32,13 @@ class Builder '==' => ':field = ?', '!=' => ':field <> ?', '>=' => ':field >= ?', + '>=' => ':field >= ?', '>' => ':field > ?', + '>' => ':field > ?', '<=' => ':field <= ?', + '<=' => ':field <= ?', '<' => ':field < ?', + '<' => ':field < ?', '{}' => ':field IN (?)', '!{}' => ':field NOT IN (?)', '()' => ':field IN (?)', diff --git a/app/code/Magento/Rule/Test/Unit/Model/Condition/Sql/BuilderTest.php b/app/code/Magento/Rule/Test/Unit/Model/Condition/Sql/BuilderTest.php index 0a2767a94668a..7be94bf690e8b 100644 --- a/app/code/Magento/Rule/Test/Unit/Model/Condition/Sql/BuilderTest.php +++ b/app/code/Magento/Rule/Test/Unit/Model/Condition/Sql/BuilderTest.php @@ -72,4 +72,69 @@ public function testAttachConditionToCollection() $this->_builder->attachConditionToCollection($collection, $combine); } + + /** + * Test for attach condition to collection with operator in html format. + * + * @covers \Magento\Rule\Model\Condition\Sql\Builder::attachConditionToCollection() + * @return void + */ + public function testAttachConditionAsHtmlToCollection() + { + $abstractCondition = $this->getMockForAbstractClass( + \Magento\Rule\Model\Condition\AbstractCondition::class, + [], + '', + false, + false, + true, + ['getOperatorForValidate', 'getMappedSqlField', 'getAttribute', 'getBindArgumentValue'] + ); + + $abstractCondition->expects($this->once())->method('getMappedSqlField')->will($this->returnValue('argument')); + $abstractCondition->expects($this->once())->method('getOperatorForValidate')->will($this->returnValue('>')); + $abstractCondition->expects($this->at(1))->method('getAttribute')->will($this->returnValue('attribute')); + $abstractCondition->expects($this->at(2))->method('getAttribute')->will($this->returnValue('attribute')); + $abstractCondition->expects($this->once())->method('getBindArgumentValue')->will($this->returnValue(10)); + + $conditions = [$abstractCondition]; + $collection = $this->createPartialMock( + \Magento\Eav\Model\Entity\Collection\AbstractCollection::class, + [ + 'getResource', + 'getSelect', + ] + ); + $combine = $this->createPartialMock( + \Magento\Rule\Model\Condition\Combine::class, + [ + 'getConditions', + 'getValue', + 'getAggregator', + ] + ); + + $resource = $this->createPartialMock(\Magento\Framework\DB\Adapter\Pdo\Mysql::class, ['getConnection']); + $select = $this->createPartialMock(\Magento\Framework\DB\Select::class, ['where']); + $select->expects($this->never())->method('where'); + + $connection = $this->getMockForAbstractClass( + \Magento\Framework\DB\Adapter\AdapterInterface::class, + ['quoteInto'], + '', + false + ); + + $connection->expects($this->once())->method('quoteInto')->with(' > ?', 10)->will($this->returnValue(' > 10')); + $collection->expects($this->once())->method('getResource')->will($this->returnValue($resource)); + $resource->expects($this->once())->method('getConnection')->will($this->returnValue($connection)); + $combine->expects($this->once())->method('getValue')->willReturn('attribute'); + $combine->expects($this->once())->method('getAggregator')->willReturn(' AND '); + $combine->expects($this->at(0))->method('getConditions')->will($this->returnValue($conditions)); + $combine->expects($this->at(1))->method('getConditions')->will($this->returnValue($conditions)); + $combine->expects($this->at(2))->method('getConditions')->will($this->returnValue($conditions)); + $combine->expects($this->at(3))->method('getConditions')->will($this->returnValue($conditions)); + + $this->_builder->attachConditionToCollection($collection, $combine); + } } diff --git a/app/code/Magento/Sales/Block/Adminhtml/Order/Create/Form/Account.php b/app/code/Magento/Sales/Block/Adminhtml/Order/Create/Form/Account.php index 0f92fa2320e89..b61a5cf83734b 100644 --- a/app/code/Magento/Sales/Block/Adminhtml/Order/Create/Form/Account.php +++ b/app/code/Magento/Sales/Block/Adminhtml/Order/Create/Form/Account.php @@ -11,6 +11,7 @@ use Magento\Framework\Api\ExtensibleDataObjectConverter; use Magento\Framework\Data\Form\Element\AbstractElement; use Magento\Framework\Pricing\PriceCurrencyInterface; +use Magento\Store\Model\ScopeInterface; /** * Create order account form @@ -134,15 +135,8 @@ protected function _prepareForm() $this->_addAttributesToForm($attributes, $fieldset); $this->_form->addFieldNameSuffix('order[account]'); - - $formValues = $this->getFormValues(); - foreach ($attributes as $code => $attribute) { - $defaultValue = $attribute->getDefaultValue(); - if (isset($defaultValue) && !isset($formValues[$code])) { - $formValues[$code] = $defaultValue; - } - } - $this->_form->setValues($formValues); + $storeId = (int)$this->_sessionQuote->getStoreId(); + $this->_form->setValues($this->extractValuesFromAttributes($attributes, $storeId)); return $this; } @@ -189,4 +183,42 @@ public function getFormValues() return $data; } + + /** + * Extract the form values from attributes. + * + * @param array $attributes + * @param int $storeId + * @return array + */ + private function extractValuesFromAttributes(array $attributes, int $storeId): array + { + $formValues = $this->getFormValues(); + foreach ($attributes as $code => $attribute) { + $defaultValue = $attribute->getDefaultValue(); + if (isset($defaultValue) && !isset($formValues[$code])) { + $formValues[$code] = $defaultValue; + } + if ($code === 'group_id' && empty($defaultValue)) { + $formValues[$code] = $this->getDefaultCustomerGroup($storeId); + } + } + + return $formValues; + } + + /** + * Gets default customer group. + * + * @param int $storeId + * @return string|null + */ + private function getDefaultCustomerGroup(int $storeId) + { + return $this->_scopeConfig->getValue( + 'customer/create_account/default_group', + ScopeInterface::SCOPE_STORE, + $storeId + ); + } } 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 34d7a3f8ee25e..8179c0e8d282a 100644 --- a/app/code/Magento/Sales/Block/Adminhtml/Order/Create/Sidebar/Cart.php +++ b/app/code/Magento/Sales/Block/Adminhtml/Order/Create/Sidebar/Cart.php @@ -3,8 +3,13 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ +declare(strict_types=1); + namespace Magento\Sales\Block\Adminhtml\Order\Create\Sidebar; +use Magento\Catalog\Model\Product; +use Magento\Catalog\Pricing\Price\FinalPrice; + /** * Adminhtml sales order create sidebar cart block * @@ -58,6 +63,17 @@ public function getItemCollection() return $collection; } + /** + * @inheritdoc + */ + public function getItemPrice(Product $product) + { + $customPrice = $this->getCartItemCustomPrice($product); + $price = $customPrice ?? $product->getPriceInfo()->getPrice(FinalPrice::PRICE_CODE)->getValue(); + + return $this->convertPrice($price); + } + /** * Retrieve display item qty availability * @@ -111,4 +127,23 @@ protected function _prepareLayout() return parent::_prepareLayout(); } + + /** + * Returns cart item custom price. + * + * @param Product $product + * @return float|null + */ + private function getCartItemCustomPrice(Product $product) + { + $items = $this->getItemCollection(); + foreach ($items as $item) { + $productItemId = $this->getProduct($item)->getId(); + if ($productItemId === $product->getId() && $item->getCustomPrice()) { + return (float)$item->getCustomPrice(); + } + } + + return null; + } } diff --git a/app/code/Magento/Sales/Block/Order/Item/Renderer/DefaultRenderer.php b/app/code/Magento/Sales/Block/Order/Item/Renderer/DefaultRenderer.php index 4c6a2b586cfc4..ad6e0082929ac 100644 --- a/app/code/Magento/Sales/Block/Order/Item/Renderer/DefaultRenderer.php +++ b/app/code/Magento/Sales/Block/Order/Item/Renderer/DefaultRenderer.php @@ -278,4 +278,21 @@ public function getItemRowTotalAfterDiscountHtml($item = null) $block->setItem($item); return $block->toHtml(); } + + /** + * Return the base total amount minus discount. + * + * @param OrderItem|InvoiceItem|CreditmemoItem $item + * @return float|null + */ + public function getBaseTotalAmount($item) + { + $baseTotalAmount = $item->getBaseRowTotal() + + $item->getBaseTaxAmount() + + $item->getBaseDiscountTaxCompensationAmount() + + $item->getBaseWeeeTaxAppliedAmount() + - $item->getBaseDiscountAmount(); + + return $baseTotalAmount; + } } diff --git a/app/code/Magento/Sales/Controller/Adminhtml/Order/AddComment.php b/app/code/Magento/Sales/Controller/Adminhtml/Order/AddComment.php index 12038ee375059..88e05a80f3797 100644 --- a/app/code/Magento/Sales/Controller/Adminhtml/Order/AddComment.php +++ b/app/code/Magento/Sales/Controller/Adminhtml/Order/AddComment.php @@ -9,17 +9,27 @@ use Magento\Backend\App\Action; use Magento\Sales\Model\Order\Email\Sender\OrderCommentSender; +/** + * Controller to execute Adding Comments. + */ class AddComment extends \Magento\Sales\Controller\Adminhtml\Order { /** - * Authorization level of a basic admin session + * Authorization level of a basic admin session. * * @see _isAllowed() */ const ADMIN_RESOURCE = 'Magento_Sales::comment'; /** - * Add order comment action + * ACL resource needed to send comment email notification. + * + * @see _isAllowed() + */ + const ADMIN_SALES_EMAIL_RESOURCE = 'Magento_Sales::emails'; + + /** + * Add order comment action. * * @return \Magento\Framework\Controller\ResultInterface */ @@ -33,8 +43,12 @@ public function execute() throw new \Magento\Framework\Exception\LocalizedException(__('Please enter a comment.')); } - $notify = isset($data['is_customer_notified']) ? $data['is_customer_notified'] : false; - $visible = isset($data['is_visible_on_front']) ? $data['is_visible_on_front'] : false; + $notify = $data['is_customer_notified'] ?? false; + $visible = $data['is_visible_on_front'] ?? false; + + if ($notify && !$this->_authorization->isAllowed(self::ADMIN_SALES_EMAIL_RESOURCE)) { + $notify = false; + } $history = $order->addStatusHistoryComment($data['comment'], $data['status']); $history->setIsVisibleOnFront($visible); @@ -59,9 +73,11 @@ public function execute() if (is_array($response)) { $resultJson = $this->resultJsonFactory->create(); $resultJson->setData($response); + return $resultJson; } } + return $this->resultRedirectFactory->create()->setPath('sales/*/'); } } diff --git a/app/code/Magento/Sales/Model/CronJob/CleanExpiredOrders.php b/app/code/Magento/Sales/Model/CronJob/CleanExpiredOrders.php index 8a7bd0260df0f..999bb1786cf83 100644 --- a/app/code/Magento/Sales/Model/CronJob/CleanExpiredOrders.php +++ b/app/code/Magento/Sales/Model/CronJob/CleanExpiredOrders.php @@ -3,11 +3,19 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ +declare(strict_types=1); + namespace Magento\Sales\Model\CronJob; +use Magento\Framework\App\ObjectManager; +use Magento\Sales\Api\OrderManagementInterface; +use Magento\Sales\Model\ResourceModel\Order\CollectionFactory; use Magento\Store\Model\StoresConfig; use Magento\Sales\Model\Order; +/** + * Class that provides functionality of cleaning expired quotes by cron + */ class CleanExpiredOrders { /** @@ -16,20 +24,28 @@ class CleanExpiredOrders protected $storesConfig; /** - * @var \Magento\Sales\Model\ResourceModel\Order\CollectionFactory + * @var CollectionFactory */ protected $orderCollectionFactory; + /** + * @var OrderManagementInterface + */ + private $orderManagement; + /** * @param StoresConfig $storesConfig - * @param \Magento\Sales\Model\ResourceModel\Order\CollectionFactory $collectionFactory + * @param CollectionFactory $collectionFactory + * @param OrderManagementInterface|null $orderManagement */ public function __construct( StoresConfig $storesConfig, - \Magento\Sales\Model\ResourceModel\Order\CollectionFactory $collectionFactory + CollectionFactory $collectionFactory, + OrderManagementInterface $orderManagement = null ) { $this->storesConfig = $storesConfig; $this->orderCollectionFactory = $collectionFactory; + $this->orderManagement = $orderManagement ?: ObjectManager::getInstance()->get(OrderManagementInterface::class); } /** @@ -48,8 +64,10 @@ public function execute() $orders->getSelect()->where( new \Zend_Db_Expr('TIME_TO_SEC(TIMEDIFF(CURRENT_TIMESTAMP, `updated_at`)) >= ' . $lifetime * 60) ); - $orders->walk('cancel'); - $orders->walk('save'); + + foreach ($orders->getAllIds() as $entityId) { + $this->orderManagement->cancel((int) $entityId); + } } } } diff --git a/app/code/Magento/Sales/Model/Order.php b/app/code/Magento/Sales/Model/Order.php index d20493060c3fd..9d66433be8f3e 100644 --- a/app/code/Magento/Sales/Model/Order.php +++ b/app/code/Magento/Sales/Model/Order.php @@ -12,6 +12,7 @@ use Magento\Framework\Locale\ResolverInterface; use Magento\Framework\Pricing\PriceCurrencyInterface; use Magento\Sales\Api\Data\OrderInterface; +use Magento\Sales\Api\Data\OrderItemInterface; use Magento\Sales\Api\Data\OrderStatusHistoryInterface; use Magento\Sales\Model\Order\Payment; use Magento\Sales\Model\Order\ProductOption; @@ -665,8 +666,8 @@ private function canCreditmemoForZeroTotalRefunded(float $totalRefunded): bool $isRefundZero = abs($totalRefunded) < .0001; // Case when Adjustment Fee (adjustment_negative) has been used for first creditmemo $hasAdjustmentFee = abs($totalRefunded - $this->getAdjustmentNegative()) < .0001; - $hasActinFlag = $this->getActionFlag(self::ACTION_FLAG_EDIT) === false; - if ($isRefundZero || $hasAdjustmentFee || $hasActinFlag) { + $hasActionFlag = $this->getActionFlag(self::ACTION_FLAG_EDIT) === false; + if ($isRefundZero || $hasAdjustmentFee || $hasActionFlag) { return false; } @@ -682,15 +683,15 @@ private function canCreditmemoForZeroTotalRefunded(float $totalRefunded): bool private function canCreditmemoForZeroTotal(float $totalRefunded): bool { $totalPaid = $this->getTotalPaid(); - //check if total paid is less than grandtotal + //check if total paid is less than grand total $checkAmtTotalPaid = $totalPaid <= $this->getGrandTotal(); //case when amount is due for invoice - $dueAmountCondition = $this->canInvoice() && $checkAmtTotalPaid; + $hasDueAmount = $this->canInvoice() && $checkAmtTotalPaid; //case when paid amount is refunded and order has creditmemo created $creditmemos = ($this->getCreditmemosCollection() === false) ? true : (count($this->getCreditmemosCollection()) > 0); $paidAmtIsRefunded = $this->getTotalRefunded() == $totalPaid && $creditmemos; - if (($dueAmountCondition || $paidAmtIsRefunded) + if (($hasDueAmount || $paidAmtIsRefunded) || (!$checkAmtTotalPaid && abs($totalRefunded - $this->getAdjustmentNegative()) < .0001) ) { return false; @@ -750,7 +751,7 @@ public function canComment() } /** - * Retrieve order shipment availability + * Retrieve order shipment availability. * * @return bool * @SuppressWarnings(PHPMD.CyclomaticComplexity) @@ -770,13 +771,29 @@ public function canShip() } foreach ($this->getAllItems() as $item) { - if ($item->getQtyToShip() > 0 && !$item->getIsVirtual() && !$item->getLockedDoShip()) { + if ($item->getQtyToShip() > 0 + && !$item->getIsVirtual() + && !$item->getLockedDoShip() + && !$this->isRefunded($item) + ) { return true; } } + return false; } + /** + * Check if item is refunded. + * + * @param OrderItemInterface $item + * @return bool + */ + private function isRefunded(OrderItemInterface $item): bool + { + return $item->getQtyRefunded() == $item->getQtyOrdered(); + } + /** * Retrieve order edit availability * @@ -1383,7 +1400,7 @@ public function getParentItemsRandomCollection($limit = 1) } /** - * Get random items collection with or without related children + * Get random items collection with or without related children. * * @param int $limit * @param bool $nonChildrenOnly @@ -1391,7 +1408,10 @@ public function getParentItemsRandomCollection($limit = 1) */ protected function _getItemsRandomCollection($limit, $nonChildrenOnly = false) { - $collection = $this->_orderItemCollectionFactory->create()->setOrderFilter($this)->setRandomOrder(); + $collection = $this->_orderItemCollectionFactory->create() + ->setOrderFilter($this) + ->setRandomOrder() + ->setPageSize($limit); if ($nonChildrenOnly) { $collection->filterByParent(); @@ -1405,9 +1425,7 @@ protected function _getItemsRandomCollection($limit, $nonChildrenOnly = false) $products )->setVisibility( $this->_productVisibility->getVisibleInSiteIds() - )->addPriceData()->setPageSize( - $limit - )->load(); + )->addPriceData()->load(); foreach ($collection as $item) { $product = $productsCollection->getItemById($item->getProductId()); @@ -2130,11 +2148,18 @@ public function getStatusHistories() /** * @inheritdoc * - * @return \Magento\Sales\Api\Data\OrderExtensionInterface|null + * @return \Magento\Sales\Api\Data\OrderExtensionInterface */ public function getExtensionAttributes() { - return $this->_getExtensionAttributes(); + $extensionAttributes = $this->_getExtensionAttributes(); + if (null === $extensionAttributes) { + /** @var \Magento\Sales\Api\Data\OrderExtensionInterface $extensionAttributes */ + $extensionAttributes = $this->extensionAttributesFactory->create(OrderInterface::class); + $this->setExtensionAttributes($extensionAttributes); + } + + return $extensionAttributes; } /** diff --git a/app/code/Magento/Sales/Model/Order/Creditmemo/Total/Tax.php b/app/code/Magento/Sales/Model/Order/Creditmemo/Total/Tax.php index 5ab9469441bef..d4c2e7b2d6854 100644 --- a/app/code/Magento/Sales/Model/Order/Creditmemo/Total/Tax.php +++ b/app/code/Magento/Sales/Model/Order/Creditmemo/Total/Tax.php @@ -75,10 +75,11 @@ public function collect(\Magento\Sales\Model\Order\Creditmemo $creditmemo) } $isPartialShippingRefunded = false; + $baseOrderShippingAmount = (float)$order->getBaseShippingAmount(); if ($invoice = $creditmemo->getInvoice()) { //recalculate tax amounts in case if refund shipping value was changed - if ($order->getBaseShippingAmount() && $creditmemo->getBaseShippingAmount()) { - $taxFactor = $creditmemo->getBaseShippingAmount() / $order->getBaseShippingAmount(); + if ($baseOrderShippingAmount && $creditmemo->getBaseShippingAmount() !== null) { + $taxFactor = $creditmemo->getBaseShippingAmount() / $baseOrderShippingAmount; $shippingTaxAmount = $invoice->getShippingTaxAmount() * $taxFactor; $baseShippingTaxAmount = $invoice->getBaseShippingTaxAmount() * $taxFactor; $totalDiscountTaxCompensation += $invoice->getShippingDiscountTaxCompensationAmount() * $taxFactor; @@ -96,7 +97,6 @@ public function collect(\Magento\Sales\Model\Order\Creditmemo $creditmemo) } } else { $orderShippingAmount = $order->getShippingAmount(); - $baseOrderShippingAmount = $order->getBaseShippingAmount(); $baseOrderShippingRefundedAmount = $order->getBaseShippingRefunded(); diff --git a/app/code/Magento/Sales/Model/Order/CreditmemoFactory.php b/app/code/Magento/Sales/Model/Order/CreditmemoFactory.php index 50523015d87eb..da41f99a65c83 100644 --- a/app/code/Magento/Sales/Model/Order/CreditmemoFactory.php +++ b/app/code/Magento/Sales/Model/Order/CreditmemoFactory.php @@ -200,6 +200,7 @@ protected function initData($creditmemo, $data) { if (isset($data['shipping_amount'])) { $creditmemo->setBaseShippingAmount((double)$data['shipping_amount']); + $creditmemo->setBaseShippingInclTax((double)$data['shipping_amount']); } if (isset($data['adjustment_positive'])) { $creditmemo->setAdjustmentPositive($data['adjustment_positive']); @@ -210,6 +211,8 @@ protected function initData($creditmemo, $data) } /** + * Calculate product options. + * * @param Item $orderItem * @param int $parentQty * @return int diff --git a/app/code/Magento/Sales/Model/Order/CustomerAssignment.php b/app/code/Magento/Sales/Model/Order/CustomerAssignment.php new file mode 100644 index 0000000000000..8bcfc1dc49de4 --- /dev/null +++ b/app/code/Magento/Sales/Model/Order/CustomerAssignment.php @@ -0,0 +1,59 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Sales\Model\Order; + +use Magento\Sales\Api\Data\OrderInterface; +use Magento\Sales\Api\OrderRepositoryInterface; +use Magento\Customer\Api\Data\CustomerInterface; +use Magento\Framework\Event\ManagerInterface; + +class CustomerAssignment +{ + /** + * @var ManagerInterface + */ + private $eventManager; + + /** + * @var OrderRepositoryInterface + */ + private $orderRepository; + + /** + * CustomerAssignment constructor. + * + * @param ManagerInterface $eventManager + * @param OrderRepositoryInterface $orderRepository + */ + public function __construct( + ManagerInterface $eventManager, + OrderRepositoryInterface $orderRepository + ) { + $this->eventManager = $eventManager; + $this->orderRepository = $orderRepository; + } + + /** + * @param OrderInterface $order + * @param CustomerInterface $customer + */ + public function execute(OrderInterface $order, CustomerInterface $customer)/*: void*/ + { + $order->setCustomerId($customer->getId()); + $order->setCustomerIsGuest(false); + $this->orderRepository->save($order); + + $this->eventManager->dispatch( + 'sales_order_customer_assign_after', + [ + 'order' => $order, + 'customer' => $customer + ] + ); + } +} diff --git a/app/code/Magento/Sales/Model/Order/Webapi/ChangeOutputArray.php b/app/code/Magento/Sales/Model/Order/Webapi/ChangeOutputArray.php new file mode 100644 index 0000000000000..a1015c102b3af --- /dev/null +++ b/app/code/Magento/Sales/Model/Order/Webapi/ChangeOutputArray.php @@ -0,0 +1,59 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Sales\Model\Order\Webapi; + +use Magento\Sales\Api\Data\OrderItemInterface; +use Magento\Sales\Block\Adminhtml\Items\Column\DefaultColumn; +use Magento\Sales\Block\Order\Item\Renderer\DefaultRenderer; + +/** + * Class for changing row total in response. + */ +class ChangeOutputArray +{ + /** + * @var DefaultColumn + */ + private $priceRenderer; + + /** + * @var DefaultRenderer + */ + private $defaultRenderer; + + /** + * @param DefaultColumn $priceRenderer + * @param DefaultRenderer $defaultRenderer + */ + public function __construct( + DefaultColumn $priceRenderer, + DefaultRenderer $defaultRenderer + ) { + $this->priceRenderer = $priceRenderer; + $this->defaultRenderer = $defaultRenderer; + } + + /** + * Changing row total for webapi order item response. + * + * @param OrderItemInterface $dataObject + * @param array $result + * @return array + */ + public function execute( + OrderItemInterface $dataObject, + array $result + ): array { + $result[OrderItemInterface::ROW_TOTAL] = $this->priceRenderer->getTotalAmount($dataObject); + $result[OrderItemInterface::BASE_ROW_TOTAL] = $this->priceRenderer->getBaseTotalAmount($dataObject); + $result[OrderItemInterface::ROW_TOTAL_INCL_TAX] = $this->defaultRenderer->getTotalAmount($dataObject); + $result[OrderItemInterface::BASE_ROW_TOTAL_INCL_TAX] = $this->defaultRenderer->getBaseTotalAmount($dataObject); + + return $result; + } +} diff --git a/app/code/Magento/Sales/Model/OrderRepository.php b/app/code/Magento/Sales/Model/OrderRepository.php index 691b803166050..6b1228ebd52b7 100644 --- a/app/code/Magento/Sales/Model/OrderRepository.php +++ b/app/code/Magento/Sales/Model/OrderRepository.php @@ -17,6 +17,10 @@ use Magento\Sales\Api\Data\ShippingAssignmentInterface; use Magento\Sales\Model\Order\ShippingAssignmentBuilder; use Magento\Sales\Model\ResourceModel\Metadata; +use Magento\Tax\Api\OrderTaxManagementInterface; +use Magento\Payment\Api\Data\PaymentAdditionalInfoInterface; +use Magento\Payment\Api\Data\PaymentAdditionalInfoInterfaceFactory; +use Magento\Framework\Serialize\Serializer\Json as JsonSerializer; /** * Repository class @@ -60,6 +64,21 @@ class OrderRepository implements \Magento\Sales\Api\OrderRepositoryInterface */ private $extensionAttributesJoinProcessor; + /** + * @var OrderTaxManagementInterface + */ + private $orderTaxManagement; + + /** + * @var PaymentAdditionalInfoFactory + */ + private $paymentAdditionalInfoFactory; + + /** + * @var JsonSerializer + */ + private $serializer; + /** * Constructor * @@ -68,13 +87,19 @@ class OrderRepository implements \Magento\Sales\Api\OrderRepositoryInterface * @param CollectionProcessorInterface|null $collectionProcessor * @param \Magento\Sales\Api\Data\OrderExtensionFactory|null $orderExtensionFactory * @param JoinProcessorInterface $extensionAttributesJoinProcessor + * @param OrderTaxManagementInterface|null $orderTaxManagement + * @param PaymentAdditionalInfoInterfaceFactory|null $paymentAdditionalInfoFactory + * @param JsonSerializer|null $serializer */ public function __construct( Metadata $metadata, SearchResultFactory $searchResultFactory, CollectionProcessorInterface $collectionProcessor = null, \Magento\Sales\Api\Data\OrderExtensionFactory $orderExtensionFactory = null, - JoinProcessorInterface $extensionAttributesJoinProcessor = null + JoinProcessorInterface $extensionAttributesJoinProcessor = null, + OrderTaxManagementInterface $orderTaxManagement = null, + PaymentAdditionalInfoInterfaceFactory $paymentAdditionalInfoFactory = null, + JsonSerializer $serializer = null ) { $this->metadata = $metadata; $this->searchResultFactory = $searchResultFactory; @@ -84,10 +109,16 @@ public function __construct( ->get(\Magento\Sales\Api\Data\OrderExtensionFactory::class); $this->extensionAttributesJoinProcessor = $extensionAttributesJoinProcessor ?: ObjectManager::getInstance()->get(JoinProcessorInterface::class); + $this->orderTaxManagement = $orderTaxManagement ?: ObjectManager::getInstance() + ->get(OrderTaxManagementInterface::class); + $this->paymentAdditionalInfoFactory = $paymentAdditionalInfoFactory ?: ObjectManager::getInstance() + ->get(PaymentAdditionalInfoInterfaceFactory::class); + $this->serializer = $serializer ?: ObjectManager::getInstance() + ->get(JsonSerializer::class); } /** - * load entity + * Load entity. * * @param int $id * @return \Magento\Sales\Api\Data\OrderInterface @@ -105,12 +136,65 @@ public function get($id) if (!$entity->getEntityId()) { throw new NoSuchEntityException(__('Requested entity doesn\'t exist')); } + $this->setOrderTaxDetails($entity); $this->setShippingAssignments($entity); + $this->setPaymentAdditionalInfo($entity); $this->registry[$id] = $entity; } return $this->registry[$id]; } + /** + * Set order tax details to extension attributes. + * + * @param OrderInterface $order + * @return void + */ + private function setOrderTaxDetails(OrderInterface $order) + { + $extensionAttributes = $order->getExtensionAttributes(); + $orderTaxDetails = $this->orderTaxManagement->getOrderTaxDetails($order->getEntityId()); + $appliedTaxes = $orderTaxDetails->getAppliedTaxes(); + + $extensionAttributes->setAppliedTaxes($appliedTaxes); + if (!empty($appliedTaxes)) { + $extensionAttributes->setConvertingFromQuote(true); + } + + $items = $orderTaxDetails->getItems(); + $extensionAttributes->setItemAppliedTaxes($items); + + $order->setExtensionAttributes($extensionAttributes); + } + + /** + * Set payment additional info to the order. + * + * @param OrderInterface $order + * @return void + */ + private function setPaymentAdditionalInfo(OrderInterface $order) + { + $extensionAttributes = $order->getExtensionAttributes(); + $paymentAdditionalInformation = $order->getPayment()->getAdditionalInformation(); + + $objects = []; + foreach ($paymentAdditionalInformation as $key => $value) { + /** @var PaymentAdditionalInfoInterface $additionalInformationObject */ + $additionalInformationObject = $this->paymentAdditionalInfoFactory->create(); + $additionalInformationObject->setKey($key); + + if (!is_string($value)) { + $value = $this->serializer->serialize($value); + } + + $additionalInformationObject->setValue($value); + $objects[] = $additionalInformationObject; + } + $extensionAttributes->setPaymentAdditionalInfo($objects); + $order->setExtensionAttributes($extensionAttributes); + } + /** * Find entities by criteria * @@ -126,6 +210,8 @@ public function getList(\Magento\Framework\Api\SearchCriteriaInterface $searchCr $searchResult->setSearchCriteria($searchCriteria); foreach ($searchResult->getItems() as $order) { $this->setShippingAssignments($order); + $this->setOrderTaxDetails($order); + $this->setPaymentAdditionalInfo($order); } return $searchResult; } @@ -179,6 +265,8 @@ public function save(\Magento\Sales\Api\Data\OrderInterface $entity) } /** + * Set shipping assignments to extension attributes. + * * @param OrderInterface $order * @return void */ diff --git a/app/code/Magento/Sales/Model/ResourceModel/Order/Handler/State.php b/app/code/Magento/Sales/Model/ResourceModel/Order/Handler/State.php index 3b127abbda732..f18118447f95f 100644 --- a/app/code/Magento/Sales/Model/ResourceModel/Order/Handler/State.php +++ b/app/code/Magento/Sales/Model/ResourceModel/Order/Handler/State.php @@ -9,12 +9,12 @@ use Magento\Sales\Model\Order; /** - * Class State + * Class to check order State. */ class State { /** - * Check order status before save + * Check order status and adjust the status before save. * * @param Order $order * @return $this @@ -23,25 +23,23 @@ class State */ public function check(Order $order) { - if (!$order->isCanceled() && !$order->canUnhold() && !$order->canInvoice() && !$order->canShip()) { - if (0 == $order->getBaseGrandTotal() || $order->canCreditmemo()) { - if ($order->getState() !== Order::STATE_COMPLETE) { - $order->setState(Order::STATE_COMPLETE) - ->setStatus($order->getConfig()->getStateDefaultStatus(Order::STATE_COMPLETE)); - } - } elseif ((float)$order->getTotalRefunded() - || !$order->getTotalRefunded() && $order->hasForcedCanCreditmemo() - ) { - if ($order->getState() !== Order::STATE_CLOSED) { - $order->setState(Order::STATE_CLOSED) - ->setStatus($order->getConfig()->getStateDefaultStatus(Order::STATE_CLOSED)); - } - } - } - if ($order->getState() == Order::STATE_NEW && $order->getIsInProcess()) { + $currentState = $order->getState(); + if ($currentState == Order::STATE_NEW && $order->getIsInProcess()) { $order->setState(Order::STATE_PROCESSING) ->setStatus($order->getConfig()->getStateDefaultStatus(Order::STATE_PROCESSING)); + $currentState = Order::STATE_PROCESSING; + } + + if (!$order->isCanceled() && !$order->canUnhold() && !$order->canInvoice()) { + if (in_array($currentState, [Order::STATE_PROCESSING, Order::STATE_COMPLETE]) && !$order->canCreditmemo()) { + $order->setState(Order::STATE_CLOSED) + ->setStatus($order->getConfig()->getStateDefaultStatus(Order::STATE_CLOSED)); + } elseif ($currentState === Order::STATE_PROCESSING && !$order->canShip()) { + $order->setState(Order::STATE_COMPLETE) + ->setStatus($order->getConfig()->getStateDefaultStatus(Order::STATE_COMPLETE)); + } } + return $this; } } diff --git a/app/code/Magento/Sales/Observer/AssignOrderToCustomerObserver.php b/app/code/Magento/Sales/Observer/AssignOrderToCustomerObserver.php index f41ea6888264f..9857fa39fa51a 100644 --- a/app/code/Magento/Sales/Observer/AssignOrderToCustomerObserver.php +++ b/app/code/Magento/Sales/Observer/AssignOrderToCustomerObserver.php @@ -12,6 +12,7 @@ use Magento\Framework\Event\Observer; use Magento\Framework\Event\ObserverInterface; use Magento\Sales\Api\OrderRepositoryInterface; +use Magento\Sales\Model\Order\CustomerAssignment; /** * Assign order to customer created after issuing guest order. @@ -24,11 +25,22 @@ class AssignOrderToCustomerObserver implements ObserverInterface private $orderRepository; /** + * @var CustomerAssignment + */ + private $assignmentService; + + /** + * AssignOrderToCustomerObserver constructor. + * * @param OrderRepositoryInterface $orderRepository + * @param CustomerAssignment $assignmentService */ - public function __construct(OrderRepositoryInterface $orderRepository) - { + public function __construct( + OrderRepositoryInterface $orderRepository, + CustomerAssignment $assignmentService + ) { $this->orderRepository = $orderRepository; + $this->assignmentService = $assignmentService; } /** @@ -44,11 +56,8 @@ public function execute(Observer $observer) if (array_key_exists('__sales_assign_order_id', $delegateData)) { $orderId = $delegateData['__sales_assign_order_id']; $order = $this->orderRepository->get($orderId); - if (!$order->getCustomerId()) { - //if customer ID wasn't already assigned then assigning. - $order->setCustomerId($customer->getId()); - $order->setCustomerIsGuest(0); - $this->orderRepository->save($order); + if (!$order->getCustomerId() && $customer->getId()) { + $this->assignmentService->execute($order, $customer); } } } diff --git a/app/code/Magento/Sales/Test/Mftf/ActionGroup/AdminInvoiceActionGroup.xml b/app/code/Magento/Sales/Test/Mftf/ActionGroup/AdminInvoiceActionGroup.xml new file mode 100644 index 0000000000000..1ce5c38256295 --- /dev/null +++ b/app/code/Magento/Sales/Test/Mftf/ActionGroup/AdminInvoiceActionGroup.xml @@ -0,0 +1,55 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> + <!--Check customer information is correct in invoice--> + <actionGroup name="VerifyBasicInvoiceInformation"> + <arguments> + <argument name="customer"/> + <argument name="shippingAddress"/> + <argument name="billingAddress"/> + <argument name="customerGroup" defaultValue="GeneralCustomerGroup"/> + </arguments> + <see selector="{{AdminInvoiceOrderAndAccountInformationSection.customerName}}" userInput="{{customer.firstname}}" stepKey="seeCustomerName"/> + <see selector="{{AdminInvoiceOrderAndAccountInformationSection.customerEmail}}" userInput="{{customer.email}}" stepKey="seeCustomerEmail"/> + <see selector="{{AdminInvoiceOrderAndAccountInformationSection.customerGroup}}" userInput="{{customerGroup.code}}" stepKey="seeCustomerGroup"/> + + <see selector="{{AdminInvoiceAddressInformationSection.billingAddress}}" userInput="{{billingAddress.street[0]}}" stepKey="seeBillingAddressStreet"/> + <see selector="{{AdminInvoiceAddressInformationSection.billingAddress}}" userInput="{{billingAddress.city}}" stepKey="seeBillingAddressCity"/> + <see selector="{{AdminInvoiceAddressInformationSection.billingAddress}}" userInput="{{billingAddress.country}}" stepKey="seeBillingAddressCountry"/> + <see selector="{{AdminInvoiceAddressInformationSection.billingAddress}}" userInput="{{billingAddress.postcode}}" stepKey="seeBillingAddressPostcode"/> + + <see selector="{{AdminInvoiceAddressInformationSection.shippingAddress}}" userInput="{{shippingAddress.street[0]}}" stepKey="seeShippingAddressStreet"/> + <see selector="{{AdminInvoiceAddressInformationSection.shippingAddress}}" userInput="{{shippingAddress.city}}" stepKey="seeShippingAddressCity"/> + <see selector="{{AdminInvoiceAddressInformationSection.shippingAddress}}" userInput="{{shippingAddress.country}}" stepKey="seeShippingAddressCountry"/> + <see selector="{{AdminInvoiceAddressInformationSection.shippingAddress}}" userInput="{{shippingAddress.postcode}}" stepKey="seeShippingAddressPostcode"/> + </actionGroup> + + <!--Check that product is in invoice items--> + <actionGroup name="SeeProductInInvoiceItems"> + <arguments> + <argument name="product"/> + </arguments> + <seeElement selector="{{AdminInvoiceItemsSection.productColumn(product.name)}}" stepKey="seeProductInInvoiceItemsGrid"/> + </actionGroup> + + <actionGroup name="StartCreateInvoiceFromOrderPage"> + <click selector="{{AdminOrderDetailsMainActionsSection.invoice}}" stepKey="clickInvoiceAction"/> + <seeInCurrentUrl url="{{AdminInvoiceNewPage.url}}" stepKey="seeNewInvoiceUrl"/> + <see selector="{{AdminHeaderSection.pageTitle}}" userInput="New Invoice" stepKey="seeNewInvoicePageTitle"/> + </actionGroup> + + <actionGroup name="SubmitInvoice"> + <click selector="{{AdminInvoiceMainActionsSection.submitInvoice}}" stepKey="clickSubmitInvoice"/> + <waitForElementVisible selector="{{AdminMessagesSection.successMessage}}" stepKey="waitForMessageAppears"/> + <see selector="{{AdminMessagesSection.successMessage}}" userInput="The invoice has been created." stepKey="seeInvoiceCreateSuccess"/> + <grabFromCurrentUrl regex="~/order_id/(\d+)/~" stepKey="grabOrderId"/> + <seeInCurrentUrl url="{{AdminOrderDetailsPage.url('$grabOrderId')}}" stepKey="seeViewOrderPageInvoice"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/Sales/Test/Mftf/ActionGroup/AdminOrderActionGroup.xml b/app/code/Magento/Sales/Test/Mftf/ActionGroup/AdminOrderActionGroup.xml index a46bf13a9688e..bb3bfdc03d340 100644 --- a/app/code/Magento/Sales/Test/Mftf/ActionGroup/AdminOrderActionGroup.xml +++ b/app/code/Magento/Sales/Test/Mftf/ActionGroup/AdminOrderActionGroup.xml @@ -60,17 +60,24 @@ <actionGroup name="navigateToNewOrderPageExistingCustomer"> <arguments> <argument name="customer"/> + <argument name="storeView" defaultValue="_defaultStore"/> </arguments> <amOnPage url="{{AdminOrdersPage.url}}" stepKey="navigateToOrderIndexPage"/> <waitForPageLoad stepKey="waitForIndexPageLoad"/> <see selector="{{AdminHeaderSection.pageTitle}}" userInput="Orders" stepKey="seeIndexPageTitle"/> <click selector="{{AdminMainActionsSection.add}}" stepKey="clickCreateNewOrder"/> <waitForPageLoad stepKey="waitForCustomerGridLoad"/> + <!--Clear grid filters--> + <conditionalClick selector="{{AdminOrderCustomersGridSection.resetButton}}" dependentSelector="{{AdminOrderCustomersGridSection.resetButton}}" visible="true" stepKey="clearExistingCustomerFilters"/> + <waitForPageLoad stepKey="waitPageLoadAfterFilterReset"/> <fillField userInput="{{customer.email}}" selector="{{AdminOrderCustomersGridSection.emailFilter}}" stepKey="filterEmail"/> <click selector="{{AdminOrderCustomersGridSection.searchButton}}" stepKey="applyFilter"/> <waitForPageLoad stepKey="waitForFilteredCustomerGridLoad"/> <click selector="{{AdminOrderCustomersGridSection.firstRow}}" stepKey="clickOnCustomer"/> - <waitForPageLoad stepKey="waitForCreateOrderPageLoad" /> + <waitForPageLoad stepKey="waitForCreateOrderPageLoad"/> + <!-- Select store view if appears --> + <conditionalClick selector="{{AdminOrderStoreScopeTreeSection.storeOption(storeView.name)}}" dependentSelector="{{AdminOrderStoreScopeTreeSection.storeOption(storeView.name)}}" visible="true" stepKey="selectStoreViewIfAppears"/> + <waitForPageLoad stepKey="waitForCreateOrderPageLoadAfterStoreSelect"/> <see selector="{{AdminHeaderSection.pageTitle}}" userInput="Create New Order" stepKey="seeNewOrderPageTitle"/> </actionGroup> <!--Add a simple product to order--> @@ -123,6 +130,35 @@ <fillField selector="{{AdminOrderFormConfigureProductSection.quantity}}" userInput="{{quantity}}" stepKey="fillQuantity"/> <click selector="{{AdminOrderFormConfigureProductSection.ok}}" stepKey="clickOkConfigurablePopover"/> </actionGroup> + <!--Add bundle product to order --> + <actionGroup name="addBundleProductToOrder"> + <arguments> + <argument name="product"/> + <argument name="quantity" type="string" defaultValue="1"/> + </arguments> + <click selector="{{AdminOrderFormItemsSection.addProducts}}" stepKey="clickAddProducts"/> + <fillField selector="{{AdminOrderFormItemsSection.skuFilter}}" userInput="{{product.sku}}" stepKey="fillSkuFilterBundle"/> + <click selector="{{AdminOrderFormItemsSection.search}}" stepKey="clickSearchBundle"/> + <scrollToTopOfPage stepKey="scrollToTop1"/> + <checkOption selector="{{AdminOrderFormItemsSection.rowCheck('1')}}" stepKey="selectBundleProduct"/> + <waitForLoadingMaskToDisappear stepKey="waitForMask"/> + <waitForElementVisible selector="{{AdminOrderFormBundleProductSection.quantity}}" stepKey="waitForBundleOptionLoad"/> + <fillField selector="{{AdminOrderFormBundleProductSection.quantity}}" userInput="{{quantity}}" stepKey="fillQuantity"/> + <click selector="{{AdminOrderFormConfigureProductSection.ok}}" stepKey="clickOk"/> + <scrollToTopOfPage stepKey="scrollToTop2"/> + <click selector="{{AdminOrderFormItemsSection.addSelected}}" stepKey="clickAddSelectedProducts"/> + </actionGroup> + <!--Add bundle product to order and check product price in the grid--> + <actionGroup name="addBundleProductToOrderAndCheckPriceInGrid" extends="addBundleProductToOrder"> + <arguments> + <argument name="price" type="string"/> + </arguments> + <grabTextFrom selector="{{AdminOrderFormItemsSection.rowPrice('1')}}" stepKey="grabProductPriceFromGrid" after="clickOk"/> + <assertEquals stepKey="assertProductPriceInGrid" message="Bundle product price in grid should be equal {{price}}" after="grabProductPriceFromGrid"> + <expectedResult type="string">{{price}}</expectedResult> + <actualResult type="variable">grabProductPriceFromGrid</actualResult> + </assertEquals> + </actionGroup> <!--Fill customer billing address--> <actionGroup name="fillOrderCustomerInformation"> <arguments> diff --git a/app/code/Magento/Sales/Test/Mftf/ActionGroup/AdminOrderGridActionGroup.xml b/app/code/Magento/Sales/Test/Mftf/ActionGroup/AdminOrderGridActionGroup.xml index 65a4b512394d8..c5307fa7ba73a 100644 --- a/app/code/Magento/Sales/Test/Mftf/ActionGroup/AdminOrderGridActionGroup.xml +++ b/app/code/Magento/Sales/Test/Mftf/ActionGroup/AdminOrderGridActionGroup.xml @@ -14,7 +14,6 @@ <argument name="orderId" type="string"/> </arguments> <amOnPage url="{{AdminOrdersPage.url}}" stepKey="navigateToOrderGridPage"/> - <waitForPageLoad stepKey="waitForOrderGridLoad"/> <conditionalClick selector="{{AdminOrdersGridSection.clearFilters}}" dependentSelector="{{AdminOrdersGridSection.clearFilters}}" visible="true" stepKey="clearExistingOrderFilters"/> <click selector="{{AdminOrdersGridSection.filters}}" stepKey="openOrderGridFilters"/> <fillField selector="{{AdminOrdersGridSection.idFilter}}" userInput="{{orderId}}" stepKey="fillOrderIdFilter"/> @@ -22,7 +21,6 @@ </actionGroup> <actionGroup name="AdminOrdersGridClearFiltersActionGroup"> <amOnPage url="{{AdminOrdersPage.url}}" stepKey="goToGridOrdersPage"/> - <waitForPageLoad stepKey="waitForPageToLoad"/> <conditionalClick selector="{{AdminOrdersGridSection.clearFilters}}" dependentSelector="{{AdminOrdersGridSection.enabledFilters}}" visible="true" stepKey="clickOnButtonToRemoveFiltersIfPresent"/> </actionGroup> </actionGroups> diff --git a/app/code/Magento/Sales/Test/Mftf/ActionGroup/StorefrontSearchGuestOrderActionGroup.xml b/app/code/Magento/Sales/Test/Mftf/ActionGroup/StorefrontSearchGuestOrderActionGroup.xml new file mode 100644 index 0000000000000..5a5943da91ce6 --- /dev/null +++ b/app/code/Magento/Sales/Test/Mftf/ActionGroup/StorefrontSearchGuestOrderActionGroup.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"> + <!--Fill order information fields and click continue--> + <actionGroup name="StorefrontSearchGuestOrderActionGroup"> + <arguments> + <argument name="orderId" type="string"/> + <argument name="orderLastName" type="string"/> + <argument name="orderEmail" type="string"/> + </arguments> + <amOnPage url="{{StorefrontGuestOrderSearchPage.url}}" stepKey="navigateToOrderAndReturnPage"/> + <fillField selector="{{StorefrontGuestOrderSearchSection.orderId}}" userInput="{{orderId}}" stepKey="fillOrderId"/> + <fillField selector="{{StorefrontGuestOrderSearchSection.billingLastName}}" userInput="{{orderLastName}}" stepKey="fillBillingLastName"/> + <fillField selector="{{StorefrontGuestOrderSearchSection.email}}" userInput="{{orderEmail}}" stepKey="fillEmail"/> + <click selector="{{StorefrontGuestOrderSearchSection.continue}}" stepKey="clickContinue"/> + <waitForPageLoad stepKey="waitForOrderInformationPageLoad"/> + <seeInCurrentUrl url="{{StorefrontGuestOrderViewPage.url}}" stepKey="seeOrderInformationUrl"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/Sales/Test/Mftf/Data/ConstData.xml b/app/code/Magento/Sales/Test/Mftf/Data/ConstData.xml new file mode 100644 index 0000000000000..523b13ae99c38 --- /dev/null +++ b/app/code/Magento/Sales/Test/Mftf/Data/ConstData.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="CONST" type="CONST"> + <data key="orderStatusComplete">Complete</data> + <data key="orderStatusClosed">Closed</data> + <data key="orderStatusPending">Pending</data> + <data key="orderStatusProcessing">Processing</data> + </entity> +</entities> + + + diff --git a/app/code/Magento/Sales/Test/Mftf/Data/SalesConfigData.xml b/app/code/Magento/Sales/Test/Mftf/Data/SalesConfigData.xml index 035c3949820f6..15f18c2ad2a6c 100644 --- a/app/code/Magento/Sales/Test/Mftf/Data/SalesConfigData.xml +++ b/app/code/Magento/Sales/Test/Mftf/Data/SalesConfigData.xml @@ -24,4 +24,23 @@ <entity name="DisableMinimumOrderCheck" type="active"> <data key="value">0</data> </entity> + + <entity name="CheckoutShippingTotalsSortOrder" type="checkout_totals_sort_order"> + <requiredEntity type="shipping">ShippingTotalsSortOrder</requiredEntity> + </entity> + <entity name="ShippingTotalsSortOrder" type="shipping"> + <data key="value">27</data> + </entity> + + <entity name="DefaultTotalsSortOrder" type="checkout_totals_sort_order"> + <requiredEntity type="shipping">DefaultShippingTotalSortOrder</requiredEntity> + </entity> + + <entity name="DefaultShippingTotalSortOrder" type="shipping"> + <requiredEntity type="shipping_inherit_value">DefaultTotalFlagDisabled</requiredEntity> + </entity> + + <entity name="DefaultTotalFlagDisabled" type="shipping_inherit_value"> + <data key="value">0</data> + </entity> </entities> diff --git a/app/code/Magento/Sales/Test/Mftf/Metadata/sales_config-meta.xml b/app/code/Magento/Sales/Test/Mftf/Metadata/sales_config-meta.xml index 77c8c8fa1c959..98c9fdb043dd6 100644 --- a/app/code/Magento/Sales/Test/Mftf/Metadata/sales_config-meta.xml +++ b/app/code/Magento/Sales/Test/Mftf/Metadata/sales_config-meta.xml @@ -20,4 +20,48 @@ </object> </object> </operation> + <operation name="SetCheckoutTotalsSortOrder" dataType="checkout_totals_sort_order" type="create" auth="adminFormKey" url="/admin/system_config/save/section/sales/" method="POST" successRegex="/messages-message-success/"> + <object key="groups" dataType="checkout_totals_sort_order"> + <object key="totals_sort" dataType="checkout_totals_sort_order"> + <object key="fields" dataType="checkout_totals_sort_order"> + <object key="subtotal" dataType="subtotal"> + <field key="value">integer</field> + <object key="inherit" dataType="subtotal_inherit_value"> + <field key="value">integer</field> + </object> + </object> + <object key="discount" dataType="discount"> + <field key="value">integer</field> + <object key="inherit" dataType="discount_inherit_value"> + <field key="value">integer</field> + </object> + </object> + <object key="shipping" dataType="shipping"> + <field key="value">integer</field> + <object key="inherit" dataType="shipping_inherit_value"> + <field key="value">integer</field> + </object> + </object> + <object key="tax" dataType="tax"> + <field key="value">integer</field> + <object key="inherit" dataType="tax_inherit_value"> + <field key="value">integer</field> + </object> + </object> + <object key="weee" dataType="weee"> + <field key="value">integer</field> + <object key="inherit" dataType="weee_inherit_value"> + <field key="value">integer</field> + </object> + </object> + <object key="grand_total" dataType="grand_total"> + <field key="value">integer</field> + <object key="inherit" dataType="grand_total_inherit_value"> + <field key="value">integer</field> + </object> + </object> + </object> + </object> + </object> + </operation> </operations> diff --git a/app/code/Magento/Sales/Test/Mftf/Page/AdminInvoiceNewPage.xml b/app/code/Magento/Sales/Test/Mftf/Page/AdminInvoiceNewPage.xml index cfd5c5794a2de..4d01371f1efc5 100644 --- a/app/code/Magento/Sales/Test/Mftf/Page/AdminInvoiceNewPage.xml +++ b/app/code/Magento/Sales/Test/Mftf/Page/AdminInvoiceNewPage.xml @@ -11,6 +11,8 @@ <page name="AdminInvoiceNewPage" url="/sales/order_invoice/new/order_id/" area="admin" module="Magento_Sales"> <section name="AdminInvoiceNewSection"/> <section name="AdminInvoiceMainActionsSection"/> + <section name="AdminInvoiceOrderAndAccountInformationSection"/> + <section name="AdminInvoiceAddressInformationSection"/> <section name="AdminInvoiceTotalSection"/> <section name="AdminInvoiceItemsSection"/> </page> diff --git a/app/code/Magento/Sales/Test/Mftf/Page/AdminOrderAddressEditPage.xml b/app/code/Magento/Sales/Test/Mftf/Page/AdminOrderAddressEditPage.xml new file mode 100644 index 0000000000000..603250e8bac7b --- /dev/null +++ b/app/code/Magento/Sales/Test/Mftf/Page/AdminOrderAddressEditPage.xml @@ -0,0 +1,15 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<pages xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Page/etc/PageObject.xsd"> + <page name="AdminOrderAddressEditPage" url="admin/sales/order/address/address_id/{{var1}}/" parameterized="true" + area="admin" module="Magento_Sales"> + <section name="AdminOrderAddressEditSection"/> + </page> +</pages> diff --git a/app/code/Magento/Sales/Test/Mftf/Page/AdminOrderCreatePage.xml b/app/code/Magento/Sales/Test/Mftf/Page/AdminOrderCreatePage.xml index 6522dbf3d25ce..e67a2c3b961d9 100644 --- a/app/code/Magento/Sales/Test/Mftf/Page/AdminOrderCreatePage.xml +++ b/app/code/Magento/Sales/Test/Mftf/Page/AdminOrderCreatePage.xml @@ -18,5 +18,6 @@ <section name="AdminOrderFormTotalSection"/> <section name="AdminOrderCustomersGridSection"/> <section name="AdminOrderFormConfigureProductSection"/> + <section name="AdminOrderStoreScopeTreeSection"/> </page> </pages> diff --git a/app/code/Magento/Sales/Test/Mftf/Page/AdminOrderDetailsPage.xml b/app/code/Magento/Sales/Test/Mftf/Page/AdminOrderDetailsPage.xml index 4280356278a88..c523bd0ed5f52 100644 --- a/app/code/Magento/Sales/Test/Mftf/Page/AdminOrderDetailsPage.xml +++ b/app/code/Magento/Sales/Test/Mftf/Page/AdminOrderDetailsPage.xml @@ -7,8 +7,8 @@ --> <pages xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" - xsi:noNamespaceSchemaLocation="../../../../../../../dev/tests/acceptance/vendor/magento/magento2-functional-testing-framework/src/Magento/FunctionalTestingFramework/Page/etc/PageObject.xsd"> - <page name="AdminOrderDetailsPage" url="sales/order/view/order_id/" area="admin" module="Magento_Sales"> + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Page/etc/PageObject.xsd"> + <page name="AdminOrderDetailsPage" url="sales/order/view/order_id/{{order_id}}/" area="admin" module="Magento_Sales" parameterized="true"> <section name="AdminMessagesSection"/> <section name="OrderDetailsMainActionsSection"/> <section name="OrderDetailsInformationSection"/> diff --git a/app/code/Magento/Sales/Test/Mftf/Page/AdminOrderInvoiceViewPage.xml b/app/code/Magento/Sales/Test/Mftf/Page/AdminOrderInvoiceViewPage.xml new file mode 100644 index 0000000000000..9f21245224748 --- /dev/null +++ b/app/code/Magento/Sales/Test/Mftf/Page/AdminOrderInvoiceViewPage.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="AdminOrderInvoiceViewPage" url="/sales/order_invoice/view/invoice_id/{{invoiceId}}" parameterized="true" area="admin" module="Magento_Sales"> + <section name="AdminOrderInvoiceViewMainActionsSection"/> + </page> +</pages> diff --git a/app/code/Magento/Sales/Test/Mftf/Page/StorefrontGuestOrderSearchPage.xml b/app/code/Magento/Sales/Test/Mftf/Page/StorefrontGuestOrderSearchPage.xml new file mode 100644 index 0000000000000..d2a4f4f0459c2 --- /dev/null +++ b/app/code/Magento/Sales/Test/Mftf/Page/StorefrontGuestOrderSearchPage.xml @@ -0,0 +1,14 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<pages xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Page/etc/PageObject.xsd"> + <page name="StorefrontGuestOrderSearchPage" url="sales/guest/form/" module="Magento_Sales" area="storefront"> + <section name="StorefrontGuestOrderSearchSection"/> + </page> +</pages> diff --git a/app/code/Magento/Sales/Test/Mftf/Page/StorefrontGuestOrderViewPage.xml b/app/code/Magento/Sales/Test/Mftf/Page/StorefrontGuestOrderViewPage.xml new file mode 100644 index 0000000000000..69c7fa76129ea --- /dev/null +++ b/app/code/Magento/Sales/Test/Mftf/Page/StorefrontGuestOrderViewPage.xml @@ -0,0 +1,14 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<pages xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Page/etc/PageObject.xsd"> + <page name="StorefrontGuestOrderViewPage" url="sales/guest/view/" module="Magento_Sales" area="storefront"> + <section name="StorefrontGuestOrderViewSection"/> + </page> +</pages> diff --git a/app/code/Magento/Sales/Test/Mftf/Section/AdminCreditMemoItemsSection.xml b/app/code/Magento/Sales/Test/Mftf/Section/AdminCreditMemoItemsSection.xml index d835bfe069683..496fae2b548a7 100644 --- a/app/code/Magento/Sales/Test/Mftf/Section/AdminCreditMemoItemsSection.xml +++ b/app/code/Magento/Sales/Test/Mftf/Section/AdminCreditMemoItemsSection.xml @@ -7,7 +7,7 @@ --> <sections xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" - xsi:noNamespaceSchemaLocation="../../../../../../../dev/tests/acceptance/vendor/magento/magento2-functional-testing-framework/src/Magento/FunctionalTestingFramework/Page/etc/SectionObject.xsd"> + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Page/etc/SectionObject.xsd"> <section name="AdminCreditMemoItemsSection"> <element name="itemQtyToRefund" type="input" selector=".order-creditmemo-tables tbody:nth-of-type({{row}}) .col-refund .qty-input" parameterized="true"/> <element name="updateQty" type="button" selector=".order-creditmemo-tables tfoot button[data-ui-id='order-items-update-button']" timeout="30"/> diff --git a/app/code/Magento/Sales/Test/Mftf/Section/AdminCreditMemoTotalSection.xml b/app/code/Magento/Sales/Test/Mftf/Section/AdminCreditMemoTotalSection.xml index 5fa5584fa8824..648f519455b56 100644 --- a/app/code/Magento/Sales/Test/Mftf/Section/AdminCreditMemoTotalSection.xml +++ b/app/code/Magento/Sales/Test/Mftf/Section/AdminCreditMemoTotalSection.xml @@ -7,9 +7,12 @@ --> <sections xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" - xsi:noNamespaceSchemaLocation="../../../../../../../dev/tests/acceptance/vendor/magento/magento2-functional-testing-framework/src/Magento/FunctionalTestingFramework/Page/etc/SectionObject.xsd"> + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Page/etc/SectionObject.xsd"> <section name="AdminCreditMemoTotalSection"> <element name="total" type="text" selector="//table[contains(@class,'order-subtotal-table')]/tbody/tr/td[contains(text(), '{{total}}')]/following-sibling::td/span/span[contains(@class, 'price')]" parameterized="true"/> - <element name="submitRefundOffline" type="button" selector=".order-totals-actions button[data-ui-id='order-items-submit-button']" timeout="30"/> + <element name="refundShipping" type="input" selector=".order-subtotal-table tbody input[name='creditmemo[shipping_amount]']"/> + <element name="updateTotals" type="button" selector=".update-totals-button" timeout="30"/> + <element name="submitRefundOffline" type="button" selector=".order-totals-actions button[title='Refund Offline']" timeout="30"/> + <element name="submitRefundOfflineEnabled" type="button" selector=".order-totals-actions button[data-ui-id='order-items-submit-button'][class='action-default scalable save submit-button primary']" timeout="30"/> </section> </sections> diff --git a/app/code/Magento/Sales/Test/Mftf/Section/AdminInvoiceAddressInformationSection.xml b/app/code/Magento/Sales/Test/Mftf/Section/AdminInvoiceAddressInformationSection.xml new file mode 100644 index 0000000000000..a3fca029096ec --- /dev/null +++ b/app/code/Magento/Sales/Test/Mftf/Section/AdminInvoiceAddressInformationSection.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="AdminInvoiceAddressInformationSection"> + <element name="billingAddress" type="text" selector=".order-billing-address address"/> + <element name="billingAddressEdit" type="button" selector=".order-billing-address .actions a"/> + <element name="shippingAddress" type="text" selector=".order-shipping-address address"/> + <element name="shippingAddressEdit" type="button" selector=".order-shipping-address .actions a"/> + </section> +</sections> diff --git a/app/code/Magento/Sales/Test/Mftf/Section/AdminInvoiceItemsSection.xml b/app/code/Magento/Sales/Test/Mftf/Section/AdminInvoiceItemsSection.xml index dbae8042b6554..0ab46bfff5d25 100644 --- a/app/code/Magento/Sales/Test/Mftf/Section/AdminInvoiceItemsSection.xml +++ b/app/code/Magento/Sales/Test/Mftf/Section/AdminInvoiceItemsSection.xml @@ -7,10 +7,12 @@ --> <sections xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" - xsi:noNamespaceSchemaLocation="../../../../../../../dev/tests/acceptance/vendor/magento/magento2-functional-testing-framework/src/Magento/FunctionalTestingFramework/Page/etc/SectionObject.xsd"> + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Page/etc/SectionObject.xsd"> <section name="AdminInvoiceItemsSection"> <element name="itemQty" type="text" selector=".order-invoice-tables tbody:nth-of-type({{row}}) .col-qty .qty-table" parameterized="true"/> <element name="itemQtyToInvoice" type="input" selector=".order-invoice-tables tbody:nth-of-type({{row}}) .col-qty-invoice .qty-input" parameterized="true"/> <element name="updateQty" type="button" selector=".order-invoice-tables tfoot button[data-ui-id='order-items-update-button']"/> + <element name="updateQtyEnabled" type="button" selector=".order-invoice-tables tfoot button[data-ui-id='order-items-update-button'][class='action-default scalable update-button']"/> + <element name="productColumn" type="text" selector="//*[contains(@class,'order-invoice-tables')]//td[@class = 'col-product']//div[contains(text(),'{{productName}}')]" parameterized="true"/> </section> </sections> diff --git a/app/code/Magento/Sales/Test/Mftf/Section/AdminInvoiceMainActionsSection.xml b/app/code/Magento/Sales/Test/Mftf/Section/AdminInvoiceMainActionsSection.xml index c1a9718b29b1c..48dc5e5b109fd 100644 --- a/app/code/Magento/Sales/Test/Mftf/Section/AdminInvoiceMainActionsSection.xml +++ b/app/code/Magento/Sales/Test/Mftf/Section/AdminInvoiceMainActionsSection.xml @@ -7,8 +7,8 @@ --> <sections xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" - xsi:noNamespaceSchemaLocation="../../../../../../../dev/tests/acceptance/vendor/magento/magento2-functional-testing-framework/src/Magento/FunctionalTestingFramework/Page/etc/SectionObject.xsd"> + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Page/etc/SectionObject.xsd"> <section name="AdminInvoiceMainActionsSection"> - <element name="submitInvoice" type="button" selector=".action-default.scalable.save.submit-button.primary"/> + <element name="submitInvoice" type="button" selector=".action-default.scalable.save.submit-button.primary" timeout="60"/> </section> </sections> diff --git a/app/code/Magento/Sales/Test/Mftf/Section/AdminInvoiceOrderAndAccountInformationSection.xml b/app/code/Magento/Sales/Test/Mftf/Section/AdminInvoiceOrderAndAccountInformationSection.xml new file mode 100644 index 0000000000000..e549f27155309 --- /dev/null +++ b/app/code/Magento/Sales/Test/Mftf/Section/AdminInvoiceOrderAndAccountInformationSection.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="AdminInvoiceOrderAndAccountInformationSection"> + <element name="customerName" type="text" selector=".order-account-information table tr:first-of-type > td span"/> + <element name="customerEmail" type="text" selector=".order-account-information table tr:nth-of-type(2) > td a"/> + <element name="customerGroup" type="text" selector=".order-account-information table tr:nth-of-type(3) > td"/> + </section> +</sections> diff --git a/app/code/Magento/Sales/Test/Mftf/Section/AdminOrderAddressEditSection.xml b/app/code/Magento/Sales/Test/Mftf/Section/AdminOrderAddressEditSection.xml new file mode 100644 index 0000000000000..5f2fbcbdde784 --- /dev/null +++ b/app/code/Magento/Sales/Test/Mftf/Section/AdminOrderAddressEditSection.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="AdminOrderAddressEditSection"> + <element name="customAttribute" type="input" selector="input#{{arg}}" parameterized="true"/> + </section> +</sections> diff --git a/app/code/Magento/Sales/Test/Mftf/Section/AdminOrderAddressInformationSection.xml b/app/code/Magento/Sales/Test/Mftf/Section/AdminOrderAddressInformationSection.xml index a9755bbbc1b61..2e9c7ee13c848 100644 --- a/app/code/Magento/Sales/Test/Mftf/Section/AdminOrderAddressInformationSection.xml +++ b/app/code/Magento/Sales/Test/Mftf/Section/AdminOrderAddressInformationSection.xml @@ -11,5 +11,6 @@ <section name="AdminOrderAddressInformationSection"> <element name="billingAddress" type="text" selector=".order-billing-address address"/> <element name="shippingAddress" type="text" selector=".order-shipping-address address"/> + <element name="editBillingAddress" type="text" selector="//div[@class='admin__page-section-item order-billing-address']//a[contains(text(),'Edit')]"/> </section> </sections> diff --git a/app/code/Magento/Sales/Test/Mftf/Section/AdminOrderFormBundleProductSection.xml b/app/code/Magento/Sales/Test/Mftf/Section/AdminOrderFormBundleProductSection.xml new file mode 100644 index 0000000000000..44a488204f775 --- /dev/null +++ b/app/code/Magento/Sales/Test/Mftf/Section/AdminOrderFormBundleProductSection.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="AdminOrderFormBundleProductSection"> + <element name="quantity" type="input" selector="#product_composite_configure_input_qty"/> + <element name="ok" type="button" selector=".modal-header .page-actions button[data-role='action']" timeout="30"/> + </section> +</sections> diff --git a/app/code/Magento/Sales/Test/Mftf/Section/AdminOrderFormItemsSection.xml b/app/code/Magento/Sales/Test/Mftf/Section/AdminOrderFormItemsSection.xml index c4cf5bd05bb6f..3cc09f6fef40f 100644 --- a/app/code/Magento/Sales/Test/Mftf/Section/AdminOrderFormItemsSection.xml +++ b/app/code/Magento/Sales/Test/Mftf/Section/AdminOrderFormItemsSection.xml @@ -14,6 +14,7 @@ <element name="skuFilter" type="input" selector="#sales_order_create_search_grid_filter_sku"/> <element name="rowCheck" type="checkbox" selector="#sales_order_create_search_grid_table > tbody tr:nth-of-type({{row}}) td.col-select [type=checkbox]" parameterized="true"/> <element name="rowQty" type="input" selector="#sales_order_create_search_grid_table > tbody tr:nth-of-type({{row}}) td.col-qty [name='qty']" parameterized="true"/> + <element name="rowPrice" type="text" selector="#sales_order_create_search_grid_table > tbody tr:nth-of-type({{row}}) td.price" parameterized="true"/> <element name="addSelected" type="button" selector="#order-search .admin__page-section-title .actions button.action-add" timeout="30"/> <element name="configure" type="button" selector=".product-configure-block button.action-default.scalable" timeout="30"/> <element name="selectProduct" type="checkbox" selector="//td[contains(text(), '{{arg}}')]/following-sibling::td[contains(@class, 'col-select col-in_products')]" parameterized="true"/> @@ -26,5 +27,7 @@ <element name="removeItems" type="select" selector="//span[text()='{{arg}}']/parent::td/following-sibling::td/select[@class='admin__control-select']" parameterized="true"/> <element name="applyCoupon" type="input" selector="#coupons:code"/> <element name="customPriceField" type="input" selector="//*[@class='custom-price-block']/following-sibling::input"/> + <element name="productRowBySku" type="block" selector="//*[@id='order-items_grid']//tbody//td[contains(@class,'col-product')]/span[starts-with(@id,'order_item_') and text()='{{productName}}']/ancestor::tr" parameterized="true"/> + <element name="itemsOrderedSummaryText" type="text" selector="#order-items_grid tfoot tr"/> </section> </sections> diff --git a/app/code/Magento/Sales/Test/Mftf/Section/AdminOrderInvoiceViewMainActionsSection.xml b/app/code/Magento/Sales/Test/Mftf/Section/AdminOrderInvoiceViewMainActionsSection.xml new file mode 100644 index 0000000000000..56ff0c8182386 --- /dev/null +++ b/app/code/Magento/Sales/Test/Mftf/Section/AdminOrderInvoiceViewMainActionsSection.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="AdminOrderInvoiceViewMainActionsSection"> + <element name="creditMemo" type="button" selector=".credit-memo" timeout="30"/> + </section> +</sections> diff --git a/app/code/Magento/Sales/Test/Mftf/Section/AdminOrderStoreScopeTreeSection.xml b/app/code/Magento/Sales/Test/Mftf/Section/AdminOrderStoreScopeTreeSection.xml new file mode 100644 index 0000000000000..cbe17499319f9 --- /dev/null +++ b/app/code/Magento/Sales/Test/Mftf/Section/AdminOrderStoreScopeTreeSection.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="AdminOrderStoreScopeTreeSection"> + <element name="storeTree" type="text" selector="div.tree-store-scope"/> + <element name="storeOption" type="radio" selector="//div[contains(@class, 'tree-store-scope')]//label[contains(text(), '{{name}}')]/preceding-sibling::input" parameterized="true" timeout="30"/> + <element name="storeForOrder" type="radio" selector="//div[contains(@class, 'tree-store-scope')]//label[contains(text(), '{{arg}}')]" parameterized="true" timeout="30"/> + </section> +</sections> diff --git a/app/code/Magento/Sales/Test/Mftf/Section/StorefrontGuestOrderSearchSection.xml b/app/code/Magento/Sales/Test/Mftf/Section/StorefrontGuestOrderSearchSection.xml new file mode 100644 index 0000000000000..b3fd2eeb05c96 --- /dev/null +++ b/app/code/Magento/Sales/Test/Mftf/Section/StorefrontGuestOrderSearchSection.xml @@ -0,0 +1,18 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<sections xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Page/etc/SectionObject.xsd"> + <section name="StorefrontGuestOrderSearchSection"> + <element name="orderId" type="input" selector="#oar-order-id"/> + <element name="billingLastName" type="input" selector="#oar-billing-lastname"/> + <element name="findOrderBy" type="select" selector="#quick-search-type-id"/> + <element name="email" type="input" selector="#oar_email"/> + <element name="continue" type="button" selector="//span[contains(text(), 'Continue')]"/> + </section> +</sections> diff --git a/app/code/Magento/Sales/Test/Mftf/Section/StorefrontGuestOrderViewSection.xml b/app/code/Magento/Sales/Test/Mftf/Section/StorefrontGuestOrderViewSection.xml new file mode 100644 index 0000000000000..31c6b4e95796e --- /dev/null +++ b/app/code/Magento/Sales/Test/Mftf/Section/StorefrontGuestOrderViewSection.xml @@ -0,0 +1,14 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<sections xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Page/etc/SectionObject.xsd"> + <section name="StorefrontGuestOrderViewSection"> + <element name="orderInformationTab" type="text" selector="//*[contains(@class,'nav')]/strong[contains(text(), 'Order Information')]"/> + </section> +</sections> diff --git a/app/code/Magento/Sales/Test/Mftf/Test/AdminAbleToShipPartiallyInvoicedItemsTest.xml b/app/code/Magento/Sales/Test/Mftf/Test/AdminAbleToShipPartiallyInvoicedItemsTest.xml index 66a9709473623..9244b77541d2d 100644 --- a/app/code/Magento/Sales/Test/Mftf/Test/AdminAbleToShipPartiallyInvoicedItemsTest.xml +++ b/app/code/Magento/Sales/Test/Mftf/Test/AdminAbleToShipPartiallyInvoicedItemsTest.xml @@ -6,7 +6,7 @@ */ --> <tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" - xsi:noNamespaceSchemaLocation="../../../../../../../dev/tests/acceptance/vendor/magento/magento2-functional-testing-framework/src/Magento/FunctionalTestingFramework/Test/etc/testSchema.xsd"> + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> <test name="AdminAbleToShipPartiallyInvoicedItemsTest"> <annotations> <title value="Ship Action is available for remaining of the partially invoiced items "/> @@ -14,6 +14,9 @@ <severity value="CRITICAL"/> <testCaseId value="MAGETWO-95524"/> <group value="sales"/> + <skip> + <issueId value="MAGETWO-97664"/> + </skip> </annotations> <before> <createData entity="_defaultCategory" stepKey="createCategory"/> @@ -56,8 +59,10 @@ <click selector="{{AdminOrderFormActionSection.submitOrder}}" after="seeCorrectGrandTotal" stepKey="clickSubmitOrder"/> <waitForLoadingMaskToDisappear stepKey="waitForLoadingMaskTodisappear"/> <waitForPageLoad stepKey="waitForOrderSubmitted"/> - <seeInCurrentUrl url="{{AdminOrderDetailsPage.url}}" after="clickSubmitOrder" stepKey="seeViewOrderPage"/> + <grabFromCurrentUrl regex="~/order_id/(\d+)/~" stepKey="grabOrderId"/> + <seeInCurrentUrl url="{{AdminOrderDetailsPage.url('$grabOrderId')}}" after="grabOrderId" stepKey="seeViewOrderPage"/> <see selector="{{AdminMessagesSection.success}}" userInput="You created the order." after="seeViewOrderPage" stepKey="seeSuccessMessage"/> + <see selector="{{AdminOrderDetailsInformationSection.orderStatus}}" userInput="{{CONST.orderStatusPending}}" stepKey="seeOrderPending"/> <grabTextFrom selector="|Order # (\d+)|" after="seeSuccessMessage" stepKey="getOrderId"/> <scrollTo selector="{{AdminOrderItemsOrderedSection.qtyColumn}}" after="getOrderId" stepKey="scrollToItemsOrdered"/> <see selector="{{AdminOrderItemsOrderedSection.itemQty('1')}}" userInput="Ordered 10" after="scrollToItemsOrdered" stepKey="seeQtyOfItemsOrdered"/> @@ -69,14 +74,16 @@ <scrollTo selector="{{AdminInvoiceItemsSection.itemQtyToInvoice('1')}}" stepKey="scrollToItemsInvoiced"/> <!--Change invoiced items count--> <fillField selector="{{AdminInvoiceItemsSection.itemQtyToInvoice('1')}}" userInput="5" stepKey="invoiceHalfTheItems"/> + <waitForElementVisible selector="{{AdminInvoiceItemsSection.updateQtyEnabled}}" stepKey="waitForEnabledQtyToBeInvoicedSubmitButton"/> <click selector="{{AdminInvoiceItemsSection.updateQty}}" stepKey="updateQtyToBeInvoiced"/> <waitForLoadingMaskToDisappear stepKey="waitForQtyToUpdate"/> <waitForElementVisible selector="{{AdminInvoiceMainActionsSection.submitInvoice}}" stepKey="waitforSubmitInvoiceBtn"/> <!--Submit Invoice--> <click selector="{{AdminInvoiceMainActionsSection.submitInvoice}}" stepKey="submitInvoice"/> - <waitForLoadingMaskToDisappear stepKey="waitForInvoiceToSubmit"/> + <waitForPageLoad stepKey="waitForInvoiceToSubmit1"/> <!--Invoice created successfully--> <see selector="{{AdminMessagesSection.success}}" userInput="The invoice has been created." stepKey="seeInvoiceSuccessMessage"/> + <see selector="{{AdminOrderDetailsInformationSection.orderStatus}}" userInput="{{CONST.orderStatusProcessing}}" stepKey="seeOrderProcessing1"/> <scrollTo selector="{{AdminOrderItemsOrderedSection.itemStatus('1')}}" stepKey="scrollToOrderItems"/> <see selector="{{AdminOrderItemsOrderedSection.itemQty('1')}}" userInput="Invoiced 5" stepKey="see5itemsInvoiced"/> <scrollTo selector="{{AdminHeaderSection.pageTitle}}" stepKey="scrollToTopOfPage"/> @@ -96,6 +103,7 @@ <waitForLoadingMaskToDisappear stepKey="waitForShipmentToSubmit"/> <!--Verify shipment created successfully--> <see selector="{{AdminMessagesSection.success}}" userInput="The shipment has been created." after="submitShipment" stepKey="successfullShipmentCreation"/> + <see selector="{{AdminOrderDetailsInformationSection.orderStatus}}" userInput="{{CONST.orderStatusProcessing}}" stepKey="seeOrderProcessing2"/> <see selector="{{AdminHeaderSection.pageTitle}}" userInput="$getOrderId" stepKey="seeOrderIdInPageTitleAfterShip"/> <scrollTo selector="{{AdminOrderItemsOrderedSection.itemStatus('1')}}" stepKey="scrollToOrderItems1"/> <see selector="{{AdminOrderItemsOrderedSection.itemQty('1')}}" userInput="Shipped 5" stepKey="see5itemsShipped"/> @@ -110,13 +118,12 @@ <see selector="{{AdminHeaderSection.pageTitle}}" userInput="New Memo" stepKey="seeNewMemoInPageTitle"/> <!--Submit refund--> <scrollTo selector="{{AdminCreditMemoItemsSection.itemQtyToRefund('1')}}" stepKey="scrollToItemsToRefund"/> - <fillField selector="{{AdminCreditMemoItemsSection.itemQtyToRefund('1')}}" userInput="5" after="scrollToItemsToRefund" stepKey="fillQtyOfItemsToRefund"/> - <click selector="{{AdminCreditMemoItemsSection.updateQty}}" stepKey="updateRefundQty"/> - <waitForLoadingMaskToDisappear stepKey="waitForRefundQtyToUpdate"/> - <waitForElementVisible selector="{{AdminCreditMemoTotalSection.submitRefundOffline}}" stepKey="seeSubmitRefundBtn"/> + <seeInField selector="{{AdminCreditMemoItemsSection.itemQtyToRefund('1')}}" userInput="5" after="scrollToItemsToRefund" stepKey="checkQtyOfItemsToRefund"/> + <waitForElementVisible selector="{{AdminCreditMemoTotalSection.submitRefundOfflineEnabled}}" stepKey="waitForSubmitRefundOfflineEnabled"/> <click selector="{{AdminCreditMemoTotalSection.submitRefundOffline}}" stepKey="submitRefundOffline"/> <!--Verify Credit Memo created successfully--> <see selector="{{AdminMessagesSection.success}}" userInput="You created the credit memo." after="submitRefundOffline" stepKey="seeCreditMemoSuccessMsg"/> + <see selector="{{AdminOrderDetailsInformationSection.orderStatus}}" userInput="{{CONST.orderStatusProcessing}}" stepKey="seeOrderProcessing3"/> <scrollTo selector="{{AdminOrderItemsOrderedSection.itemStatus('1')}}" stepKey="scrollToOrderItems2"/> <see selector="{{AdminOrderItemsOrderedSection.itemQty('1')}}" userInput="Refunded 5" stepKey="see5itemsRefunded"/> <scrollTo selector="{{AdminHeaderSection.pageTitle}}" stepKey="scrollToTopOfPage2"/> @@ -135,8 +142,10 @@ <see selector="{{AdminInvoiceItemsSection.itemQty('1')}}" userInput="Refunded 5" stepKey="seeQtyOfItemsRefunded"/> <!--Submit Invoice--> <click selector="{{AdminInvoiceMainActionsSection.submitInvoice}}" stepKey="submitInvoice2" /> + <waitForPageLoad stepKey="waitForInvoiceToSubmit2"/> <!--Invoice created successfully for the rest of the ordered items--> - <see selector="{{AdminMessagesSection.success}}" userInput="The invoice has been created." after="submitInvoice2" stepKey="seeInvoiceSuccessMessage2"/> + <see selector="{{AdminMessagesSection.success}}" userInput="The invoice has been created." stepKey="seeInvoiceSuccessMessage2"/> + <see selector="{{AdminOrderDetailsInformationSection.orderStatus}}" userInput="{{CONST.orderStatusProcessing}}" stepKey="seeOrderProcessing4"/> <scrollTo selector="{{AdminOrderItemsOrderedSection.itemStatus('1')}}" stepKey="scrollToOrderItems3"/> <see selector="{{AdminOrderItemsOrderedSection.itemQty('1')}}" userInput="Invoiced 10" stepKey="see10itemsInvoiced"/> <scrollTo selector="{{AdminHeaderSection.pageTitle}}" stepKey="scrollToTopOfPage3"/> @@ -155,6 +164,7 @@ <!--Submit Shipment--> <click selector="{{AdminShipmentMainActionsSection.submitShipment}}" after="fillRestOfItemsToShip" stepKey="submitShipment2" /> <see selector="{{AdminMessagesSection.success}}" userInput="The shipment has been created." after="submitShipment2" stepKey="successfullyCreatedShipment"/> + <see selector="{{AdminOrderDetailsInformationSection.orderStatus}}" userInput="{{CONST.orderStatusComplete}}" stepKey="seeOrderComplete"/> <!--Verify Items Status and Shipped Qty in the Items Ordered section--> <scrollTo selector="{{AdminOrderItemsOrderedSection.itemStatus('1')}}" stepKey="scrollToItemsOrdered2"/> diff --git a/app/code/Magento/Sales/Test/Mftf/Test/AdminAvailabilityCreditMemoWithNoPaymentTest.xml b/app/code/Magento/Sales/Test/Mftf/Test/AdminAvailabilityCreditMemoWithNoPaymentTest.xml index 379cb67d3e52f..9bd906a8abe08 100644 --- a/app/code/Magento/Sales/Test/Mftf/Test/AdminAvailabilityCreditMemoWithNoPaymentTest.xml +++ b/app/code/Magento/Sales/Test/Mftf/Test/AdminAvailabilityCreditMemoWithNoPaymentTest.xml @@ -72,19 +72,14 @@ <click selector="{{AdminOrderFormActionSection.submitOrder}}" after="selectFreeShippingOption" stepKey="clickSubmitOrder"/> <!--Click *Invoice* button--> - <click selector="{{AdminOrderDetailsMainActionsSection.invoice}}" stepKey="clickInvoiceButton"/> - <see selector="{{AdminHeaderSection.pageTitle}}" userInput="New Invoice" after="clickInvoiceButton" stepKey="seeNewInvoiceInPageTitle"/> - <waitForPageLoad stepKey="waitForInvoicePageOpened"/> - - <click selector="{{AdminInvoiceMainActionsSection.submitInvoice}}" stepKey="clickSubmitInvoice"/> - <waitForPageLoad stepKey="waitForInvoiceSaved"/> - <see userInput="The invoice has been created." stepKey="seeCorrectMessage"/> + <actionGroup ref="StartCreateInvoiceFromOrderPage" stepKey="startCreateInvoice"/> + <actionGroup ref="SubmitInvoice" stepKey="submitInvoice"/> <!--Verify that *Credit Memo* button is displayed--> <seeElement selector="{{AdminOrderDetailsMainActionsSection.creditMemo}}" stepKey="seeCreditMemo"/> <click selector="{{AdminOrderDetailsMainActionsSection.creditMemo}}" stepKey="clickCreditMemoItem"/> <waitForPageLoad stepKey="waitForCreditMemoPageLoaded"/> - <see userInput="New Memo" stepKey="seeNewMemoPage"/> - <seeInCurrentUrl url="{{AdminCreditMemoNewPage.url}}" stepKey="seeUrlOnPage"/> + <see selector="{{AdminHeaderSection.pageTitle}}" userInput="New Memo" stepKey="seeNewMemoPageTitle"/> + <seeInCurrentUrl url="{{AdminCreditMemoNewPage.url}}" stepKey="seeNewMemoUrlOnPage"/> </test> </tests> diff --git a/app/code/Magento/Sales/Test/Mftf/Test/AdminCheckingCreditMemoTotalsTest.xml b/app/code/Magento/Sales/Test/Mftf/Test/AdminCheckingCreditMemoTotalsTest.xml new file mode 100644 index 0000000000000..e74d6178dd8ed --- /dev/null +++ b/app/code/Magento/Sales/Test/Mftf/Test/AdminCheckingCreditMemoTotalsTest.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="AdminCheckingCreditMemoTotalsTest"> + <annotations> + <features value="CreditMemo"/> + <stories value="MAGETWO-82400: Credit Memo - Wrong tax calculation! #10982"/> + <title value="Checking Credit Memo Totals"/> + <description value="Checking Credit Memo Totals"/> + <severity value="MAJOR"/> + <testCaseId value="MAGETWO-97140"/> + <group value="sales"/> + <group value="tax"/> + <skip> + <issueId value="MAGETWO-97826"/> + </skip> + </annotations> + <before> + <!--Create category--> + <createData entity="_defaultCategory" stepKey="createCategory"/> + <!--Create simple product--> + <createData entity="_defaultProduct" stepKey="createSimpleProduct"> + <requiredEntity createDataKey="createCategory"/> + <field key="price">100</field> + </createData> + <!--Create Tax Rule--> + <createData entity="SimpleTaxRule" stepKey="createTaxRule"/> + <!--Create customer--> + <createData entity="Simple_US_CA_Customer" stepKey="createCustomer"/> + <!--Configure Tax Class for shipping--> + <createData entity="TaxClassForShippingConfig" stepKey="configureTaxClassForShipping"/> + <!--Configure Braintree--> + <createData entity="SandboxBraintreeConfig" stepKey="configureBraintree"/> + <!--Login to admin page--> + <actionGroup ref="LoginAsAdmin" stepKey="loginAsAdmin"/> + </before> + <after> + <!--Delete category--> + <deleteData createDataKey="createCategory" stepKey="deleteCategory"/> + <!--Delete simple product--> + <deleteData createDataKey="createSimpleProduct" stepKey="deleteSimpleProduct"/> + <!--Delete Tax Rule--> + <deleteData createDataKey="createTaxRule" stepKey="deleteTaxRule"/> + <!--Delete customer--> + <deleteData createDataKey="createCustomer" stepKey="deleteCustomer"/> + <!--Restore default configuration for Tax Class for shipping--> + <createData entity="DefaultTaxClassForShippingConfig" stepKey="restoreTaxClassForShippingConfig"/> + <!--Restore default configuration for Braintree--> + <createData entity="DefaultBraintreeConfig" stepKey="restoreBraintreeConfig"/> + <!--Logout from admin page--> + <actionGroup ref="logout" stepKey="logout"/> + </after> + + <!--Create new order with existing customer--> + <actionGroup ref="navigateToNewOrderPageExistingCustomer" stepKey="navigateToNewOrderPage"> + <argument name="customer" value="$$createCustomer$$"/> + </actionGroup> + <!--Add simple product to order--> + <actionGroup ref="addSimpleProductToOrder" stepKey="addSimpleProductToOrder"> + <argument name="product" value="$$createSimpleProduct$$"/> + </actionGroup> + <!--Select Flat Rate shipping method--> + <actionGroup ref="orderSelectFlatRateShipping" stepKey="selectFlatRateShipping"/> + <!--Fill Braintree credit card for payment method--> + <actionGroup ref="AdminOrderFillBraintreeCreditCardActionGroup" stepKey="fillBraintreeCreditCard"/> + <!--Submit order--> + <click selector="{{AdminOrderFormActionSection.submitOrder}}" stepKey="submitOrder"/> + <waitForPageLoad stepKey="waitForSubmitOrder"/> + <see selector="{{AdminMessagesSection.success}}" userInput="You created the order." + stepKey="seeSuccessMessage"/> + + <!--Create invoice--> + <actionGroup ref="StartCreateInvoiceFromOrderPage" stepKey="startCreateInvoice"/> + <!--Submit invoice--> + <actionGroup ref="SubmitInvoice" stepKey="submitInvoice"/> + + <!--Go to invoice page--> + <click selector="{{AdminOrderViewTabsSection.invoices}}" stepKey="clickInvoicesTab"/> + <waitForPageLoad stepKey="waitForInvoiceGridToLoad"/> + <see selector="{{AdminOrderInvoicesTabSection.gridRow('1')}}" userInput="$113.66" stepKey="seeInvoiceInGrid"/> + <click selector="{{AdminDataGridTableSection.rowViewAction('1')}}" stepKey="clickViewInvoice"/> + + <!--Create Credit Memo--> + <click selector="{{AdminOrderInvoiceViewMainActionsSection.creditMemo}}" stepKey="clickCreditMemo"/> + <see selector="{{AdminHeaderSection.pageTitle}}" userInput="New Memo" stepKey="seeNewMemoPageTitle"/> + <fillField selector="{{AdminCreditMemoTotalSection.refundShipping}}" userInput="0" stepKey="setRefundShipping"/> + <click selector="{{AdminCreditMemoTotalSection.updateTotals}}" stepKey="clickUpdateTotals"/> + <waitForLoadingMaskToDisappear stepKey="waitForUpdateTotals"/> + <click selector="{{AdminCreditMemoTotalSection.submitRefundOffline}}" stepKey="clickRefundOffline"/> + <see selector="{{AdminMessagesSection.success}}" userInput="You created the credit memo." + stepKey="seeCreatedCreditMemoSuccessMessage"/> + + <!--Go to Credit Memo tab--> + <click selector="{{AdminOrderViewTabsSection.creditMemos}}" stepKey="clickCreditMemosTab"/> + <waitForPageLoad stepKey="waitForCreditMemosGridToLoad"/> + + <!--Check refunded total --> + <see selector="{{AdminOrderCreditMemosTabSection.gridRow('1')}}" userInput="$108.25" + stepKey="seeCreditMemoInGrid"/> + </test> +</tests> diff --git a/app/code/Magento/Sales/Test/Mftf/Test/AdminCreateCreditMemoWhenCartRuleDeletedTest.xml b/app/code/Magento/Sales/Test/Mftf/Test/AdminCreateCreditMemoWhenCartRuleDeletedTest.xml new file mode 100644 index 0000000000000..5386ade5dffd2 --- /dev/null +++ b/app/code/Magento/Sales/Test/Mftf/Test/AdminCreateCreditMemoWhenCartRuleDeletedTest.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="AdminCreateCreditMemoWhenCartRuleDeletedTest"> + <annotations> + <stories value="MAGETWO-95722: Cannot create credit memo if the used in the order cart rule is deleted"/> + <title value="Checking creating of credit memo"/> + <description value="Verify Credit Memo created if the used in the order cart rule is deleted"/> + <severity value="MAJOR"/> + <testCaseId value="MAGETWO-97232"/> + <group value="sales"/> + <skip> + <issueId value="MAGETWO-97825"/> + </skip> + </annotations> + <before> + <!--Create product with category--> + <createData entity="_defaultCategory" stepKey="createCategory"/> + <createData entity="_defaultProduct" stepKey="createProduct"> + <requiredEntity createDataKey="createCategory"/> + </createData> + <!--Login as admin--> + <actionGroup ref="LoginAsAdmin" stepKey="loginAsAdmin"/> + </before> + <after> + <!--Clear filters--> + <amOnPage url="{{AdminOrdersPage.url}}" stepKey="navigateToOrderGridPage"/> + <actionGroup ref="clearFiltersAdminDataGrid" stepKey="clearOrdersFilters"/> + <amOnPage url="{{AdminCartPriceRulesPage.url}}" stepKey="amOnCartPriceRulePage"/> + <actionGroup ref="clearFiltersAdminDataGrid" stepKey="clearCartPriceRuleFilters"/> + <!--Delete product and category--> + <deleteData createDataKey="createProduct" stepKey="deleteProduct"/> + <deleteData createDataKey="createCategory" stepKey="deleteCategory"/> + <!--Log Out--> + <actionGroup ref="logout" stepKey="logOut"/> + </after> + <!--Create Cart Price Rule with a specific coupon--> + <actionGroup ref="AdminCreateCartPriceRuleSpecificCouponActionGroup" stepKey="createCartPriceRule"> + <argument name="rule" value="TestSalesRule"/> + <argument name="userPerCoupon" value="99"/> + </actionGroup> + <!--Go to Storefront. Add product to cart--> + <amOnPage url="{{StorefrontProductPage.url($$createProduct.name$$)}}" stepKey="goToProductPage"/> + <actionGroup ref="ApplyCartRuleOnStorefrontActionGroup" stepKey="addProductToCardAndApplyCartRule"> + <argument name="product" value="$$createProduct$$"/> + <argument name="couponCode" value="{{_defaultCoupon.code}}"/> + </actionGroup> + <!--Proceed to checkout--> + <actionGroup ref="GoToCheckoutFromMinicartActionGroup" stepKey="goToCheckoutPage"/> + <actionGroup ref="GuestCheckoutFillingShippingSectionActionGroup" stepKey="guestCheckoutFillingShippingSection"> + <argument name="customerVar" value="Simple_US_Customer"/> + <argument name="customerAddressVar" value="US_Address_TX"/> + <argument name="shippingMethod" value="Flat Rate"/> + </actionGroup> + <!--Select Check/Money payment--> + <actionGroup ref="CheckoutSelectCheckMoneyOrderPaymentActionGroup" stepKey="selectPaymentMethod"/> + <!--Place order--> + <actionGroup ref="ClickPlaceOrderActionGroup" stepKey="placeOrder"/> + <grabTextFrom selector="{{CheckoutSuccessMainSection.orderNumber}}" stepKey="grabOrderNumber"/> + <!--Open Order--> + <actionGroup ref="filterOrderGridById" stepKey="filterOrderGridById1"> + <argument name="orderId" value="{$grabOrderNumber}"/> + </actionGroup> + <click selector="{{AdminDataGridTableSection.firstRow}}" stepKey="clickOrderRow1"/> + <!--Create and Submit Invoice--> + <actionGroup ref="StartCreateInvoiceFromOrderPage" stepKey="createInvoice"/> + <actionGroup ref="SubmitInvoice" stepKey="submitInvoice"/> + <!--Delete the cart price rule --> + <actionGroup ref="DeleteCartPriceRuleByName" stepKey="deleteCartPriceRule"> + <argument name="ruleName" value="{{TestSalesRule.name}}"/> + </actionGroup> + <!--Open Order--> + <actionGroup ref="filterOrderGridById" stepKey="filterOrderGridById2"> + <argument name="orderId" value="{$grabOrderNumber}"/> + </actionGroup> + <click selector="{{AdminDataGridTableSection.firstRow}}" stepKey="clickOrderRow2"/> + <!--Create Credit Memo--> + <click selector="{{AdminOrderDetailsMainActionsSection.creditMemo}}" stepKey="clickCreateCreditMemo"/> + <see selector="{{AdminHeaderSection.pageTitle}}" userInput="New Memo" stepKey="seeNewMemoInPageTitle"/> + <click selector="{{AdminCreditMemoTotalSection.submitRefundOffline}}" stepKey="clickRefundOffline"/> + <!--Make sure that Credit memo was created successfully--> + <see selector="{{AdminMessagesSection.success}}" userInput="You created the credit memo." stepKey="seeCreditMemoSuccess"/> + </test> +</tests> diff --git a/app/code/Magento/Sales/Test/Mftf/Test/AdminCreateOrderWithBundleProductTest.xml b/app/code/Magento/Sales/Test/Mftf/Test/AdminCreateOrderWithBundleProductTest.xml new file mode 100644 index 0000000000000..2332ea3723a42 --- /dev/null +++ b/app/code/Magento/Sales/Test/Mftf/Test/AdminCreateOrderWithBundleProductTest.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="AdminCreateOrderWithBundleProductTest"> + <annotations> + <title value="Create Order in Admin and update bundle product configuration"/> + <stories value="MAGETWO-96394: Wrong price calculation for bundle product on creating order from the admin panel"/> + <description value="Add bundle product with bundle option items with default quantity 2 to order in Admin and check price in product grid"/> + <features value="Sales"/> + <severity value="AVERAGE"/> + <group value="Sales"/> + </annotations> + + <before> + <!--Set default flat rate shipping method settings--> + <createData entity="FlatRateShippingMethodDefault" stepKey="setDefaultFlatRateShippingMethod"/> + + <!--Create simple customer--> + <createData entity="Simple_US_CA_Customer" stepKey="simpleCustomer"/> + + <!--Create simple product 1--> + <createData entity="ApiProductWithDescription" stepKey="simple1" before="simple2"/> + + <!--Create simple product 2--> + <createData entity="ApiProductWithDescription" stepKey="simple2" before="product"/> + + <!--Create bundle product with checkbox bundle option--> + <createData entity="ApiBundleProduct" stepKey="product"/> + <createData entity="CheckboxOption" stepKey="checkboxBundleOption"> + <requiredEntity createDataKey="product"/> + </createData> + + <!--Link simple product 1 to bundle option with default quantity 2--> + <createData entity="ApiBundleLink" stepKey="createBundleLink1"> + <requiredEntity createDataKey="product"/> + <requiredEntity createDataKey="checkboxBundleOption"/> + <requiredEntity createDataKey="simple1"/> + <field key="qty">2</field> + <field key="is_default">1</field> + </createData> + + <!--Link simple product 2 to bundle option with default quantity 2--> + <createData entity="ApiBundleLink" stepKey="createBundleLink2"> + <requiredEntity createDataKey="product"/> + <requiredEntity createDataKey="checkboxBundleOption"/> + <requiredEntity createDataKey="simple2"/> + <field key="qty">2</field> + <field key="is_default">1</field> + </createData> + + <!--Add drop-down bundle option--> + <createData entity="DropDownBundleOption" stepKey="dropDownBundleOption"> + <requiredEntity createDataKey="product"/> + </createData> + + <!--Link simple product 1 to drop-down bundle option with default quantity 2--> + <createData entity="ApiBundleLink" stepKey="createBundleLink3"> + <requiredEntity createDataKey="product"/> + <requiredEntity createDataKey="dropDownBundleOption"/> + <requiredEntity createDataKey="simple1"/> + <field key="qty">2</field> + <field key="is_default">1</field> + </createData> + + <!--Link simple product 2 to drop-down bundle option with default quantity 2--> + <createData entity="ApiBundleLink" stepKey="createBundleLink4"> + <requiredEntity createDataKey="product"/> + <requiredEntity createDataKey="dropDownBundleOption"/> + <requiredEntity createDataKey="simple2"/> + <field key="qty">2</field> + </createData> + <actionGroup ref="LoginAsAdmin" stepKey="loginAsAdmin"/> + </before> + + <!--Create new customer order--> + <actionGroup ref="navigateToNewOrderPageExistingCustomer" stepKey="navigateToNewOrderWithExistingCustomer"> + <argument name="customer" value="$$simpleCustomer$$"/> + </actionGroup> + + <!--Add bundle product to order and check product price in grid--> + <actionGroup ref="addBundleProductToOrderAndCheckPriceInGrid" stepKey="addBundleProductToOrder"> + <argument name="product" value="$$product$$"/> + <argument name="quantity" value="1"/> + <argument name="price" value="$738.00"/> + </actionGroup> + + <!--Select FlatRate shipping method--> + <actionGroup ref="orderSelectFlatRateShipping" stepKey="orderSelectFlatRateShippingMethod"/> + + <!--Submit order--> + <click selector="{{AdminOrderFormActionSection.submitOrder}}" stepKey="submitOrder"/> + + <!--Verify order information--> + <actionGroup ref="verifyCreatedOrderInformation" stepKey="verifyCreatedOrderInformation"/> + + <after> + <actionGroup ref="logout" stepKey="logout"/> + + <deleteData createDataKey="product" stepKey="delete"/> + <deleteData createDataKey="simpleCustomer" stepKey="deleteSimpleCustomer"/> + <deleteData createDataKey="simple1" stepKey="deleteSimple1" before="deleteSimple2"/> + <deleteData createDataKey="simple2" stepKey="deleteSimple2" before="delete"/> + </after> + </test> +</tests> diff --git a/app/code/Magento/Sales/Test/Mftf/Test/AdminCreateOrderWithMinimumAmountEnabledTest.xml b/app/code/Magento/Sales/Test/Mftf/Test/AdminCreateOrderWithMinimumAmountEnabledTest.xml index e339f8056e96d..70b39a664c0f2 100644 --- a/app/code/Magento/Sales/Test/Mftf/Test/AdminCreateOrderWithMinimumAmountEnabledTest.xml +++ b/app/code/Magento/Sales/Test/Mftf/Test/AdminCreateOrderWithMinimumAmountEnabledTest.xml @@ -61,7 +61,8 @@ <!--Submit Order and verify information--> <click selector="{{AdminOrderFormActionSection.submitOrder}}" stepKey="clickSubmitOrder"/> - <seeInCurrentUrl url="{{AdminOrderDetailsPage.url}}" stepKey="seeViewOrderPage"/> + <grabFromCurrentUrl regex="~/order_id/(\d+)/~" stepKey="grabOrderId"/> + <seeInCurrentUrl url="{{AdminOrderDetailsPage.url('$grabOrderId')}}" stepKey="seeViewOrderPage"/> <see selector="{{AdminMessagesSection.success}}" userInput="You created the order." stepKey="seeSuccessMessage"/> <see selector="{{AdminOrderDetailsInformationSection.orderStatus}}" userInput="Pending" stepKey="seeOrderPendingStatus"/> diff --git a/app/code/Magento/Sales/Test/Mftf/Test/CreditMemoTotalAfterShippingDiscountTest.xml b/app/code/Magento/Sales/Test/Mftf/Test/AdminCreditMemoTotalAfterShippingDiscountTest.xml similarity index 91% rename from app/code/Magento/Sales/Test/Mftf/Test/CreditMemoTotalAfterShippingDiscountTest.xml rename to app/code/Magento/Sales/Test/Mftf/Test/AdminCreditMemoTotalAfterShippingDiscountTest.xml index a703f5e690cfa..2f4271d56038b 100644 --- a/app/code/Magento/Sales/Test/Mftf/Test/CreditMemoTotalAfterShippingDiscountTest.xml +++ b/app/code/Magento/Sales/Test/Mftf/Test/AdminCreditMemoTotalAfterShippingDiscountTest.xml @@ -7,8 +7,8 @@ --> <tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" - xsi:noNamespaceSchemaLocation="../../../../../../../dev/tests/acceptance/vendor/magento/magento2-functional-testing-framework/src/Magento/FunctionalTestingFramework/Test/etc/testSchema.xsd"> - <test name="CreditMemoTotalAfterShippingDiscountTest"> + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> + <test name="AdminCreditMemoTotalAfterShippingDiscountTest"> <annotations> <features value="Credit memo"/> <title value="Verify credit memo grand total after shipping discount is applied via Cart Price Rule"/> @@ -26,6 +26,8 @@ <actionGroup ref="SetTaxClassForShipping" stepKey="setShippingTaxClass"/> </before> <after> + <!--Clear filter in orders grid--> + <actionGroup ref="AdminOrdersGridClearFiltersActionGroup" stepKey="ordersGridClearFilters"/> <actionGroup ref="ResetTaxClassForShipping" stepKey="resetTaxClassForShipping"/> <actionGroup ref="DeleteCartPriceRuleByName" stepKey="deleteSalesRule"> <argument name="ruleName" value="{{ApiSalesRule.name}}"/> @@ -89,14 +91,15 @@ <actionGroup ref="LoginAsAdmin" stepKey="loginAsAdmin1"/> <!-- Search for Order in the order grid --> - <amOnPage url="{{AdminOrdersPage.url}}" stepKey="onOrdersPage"/> - <waitForLoadingMaskToDisappear stepKey="waitForLoadingMask3"/> - <fillField selector="{{OrdersGridSection.search}}" userInput="{$grabOrderNumber}" stepKey="searchOrderNum"/> - <click selector="{{OrdersGridSection.submitSearch}}" stepKey="submitSearch"/> + <actionGroup ref="filterOrderGridById" stepKey="filterOrderGridById"> + <argument name="orderId" value="$grabOrderNumber"/> + </actionGroup> <waitForLoadingMaskToDisappear stepKey="waitForLoadingMask4"/> <!-- Create invoice --> - <click selector="{{OrdersGridSection.firstRow}}" stepKey="clickOrderRow"/> + <click selector="{{AdminDataGridTableSection.firstRow}}" stepKey="clickOrderRow"/> + <waitForPageLoad stepKey="waitForPageLoad"/> + <see selector="{{AdminOrderDetailsInformationSection.orderStatus}}" userInput="{{CONST.orderStatusPending}}" stepKey="seeOrderPending"/> <click selector="{{AdminOrderDetailsMainActionsSection.invoice}}" stepKey="clickInvoiceButton"/> <see selector="{{AdminHeaderSection.pageTitle}}" userInput="New Invoice" stepKey="seeNewInvoiceInPageTitle" after="clickInvoiceButton"/> @@ -114,6 +117,7 @@ <grabTextFrom selector="{{AdminInvoiceTotalSection.grandTotal}}" stepKey="grabInvoiceGrandTotal" after="seeCorrectGrandTotal"/> <click selector="{{AdminInvoiceMainActionsSection.submitInvoice}}" stepKey="clickSubmitInvoice"/> <see selector="{{OrderDetailsMessagesSection.successMessage}}" userInput="The invoice has been created." stepKey="seeSuccessMessage1"/> + <see selector="{{AdminOrderDetailsInformationSection.orderStatus}}" userInput="{{CONST.orderStatusProcessing}}" stepKey="seeOrderProcessing"/> <!--Create Credit Memo--> <comment userInput="Admin creates credit memo" stepKey="createCreditMemoComment"/> diff --git a/app/code/Magento/Sales/Test/Mftf/Test/AdminSubmitsOrderWithAndWithoutEmailTest.xml b/app/code/Magento/Sales/Test/Mftf/Test/AdminSubmitsOrderWithAndWithoutEmailTest.xml index fcef43c81bb1d..0bc26674e7dc8 100644 --- a/app/code/Magento/Sales/Test/Mftf/Test/AdminSubmitsOrderWithAndWithoutEmailTest.xml +++ b/app/code/Magento/Sales/Test/Mftf/Test/AdminSubmitsOrderWithAndWithoutEmailTest.xml @@ -6,7 +6,7 @@ */ --> <tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" - xsi:noNamespaceSchemaLocation="../../../../../../../dev/tests/acceptance/vendor/magento/magento2-functional-testing-framework/src/Magento/FunctionalTestingFramework/Test/etc/testSchema.xsd"> + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> <test name="AdminSubmitsOrderWithAndWithoutEmailTest"> <annotations> <title value="Email is required to create an order from Admin Panel"/> @@ -69,7 +69,8 @@ <!--Submit Order and verify information--> <click selector="{{AdminOrderFormActionSection.submitOrder}}" stepKey="clickSubmitOrder" after="seeCorrectGrandTotal"/> - <seeInCurrentUrl url="{{AdminOrderDetailsPage.url}}" after="clickSubmitOrder" stepKey="seeViewOrderPage"/> + <grabFromCurrentUrl regex="~/order_id/(\d+)/~" stepKey="grabOrderId"/> + <seeInCurrentUrl url="{{AdminOrderDetailsPage.url('$grabOrderId')}}" after="grabOrderId" stepKey="seeViewOrderPage"/> <see selector="{{AdminMessagesSection.success}}" userInput="You created the order." after="seeViewOrderPage" stepKey="seeSuccessMessage"/> </test> </tests> diff --git a/app/code/Magento/Sales/Test/Unit/Block/Order/Item/Renderer/DefaultRendererTest.php b/app/code/Magento/Sales/Test/Unit/Block/Order/Item/Renderer/DefaultRendererTest.php index 9561baf6bd5f4..7863fc20b7396 100644 --- a/app/code/Magento/Sales/Test/Unit/Block/Order/Item/Renderer/DefaultRendererTest.php +++ b/app/code/Magento/Sales/Test/Unit/Block/Order/Item/Renderer/DefaultRendererTest.php @@ -60,18 +60,7 @@ protected function setUp() ->setMethods(['setItem', 'toHtml']) ->getMock(); - $itemMockMethods = [ - '__wakeup', - 'getRowTotal', - 'getTaxAmount', - 'getDiscountAmount', - 'getDiscountTaxCompensationAmount', - 'getWeeeTaxAppliedRowAmount', - ]; - $this->itemMock = $this->getMockBuilder(\Magento\Sales\Model\Order\Item::class) - ->disableOriginalConstructor() - ->setMethods($itemMockMethods) - ->getMock(); + $this->itemMock = $this->createMock(\Magento\Sales\Model\Order\Item::class); } public function testGetItemPriceHtml() @@ -161,4 +150,20 @@ public function testGetTotalAmount() $this->assertEquals($expectedResult, $this->block->getTotalAmount($this->itemMock)); } + + /** + * @return void + */ + public function testGetBaseTotalAmount() + { + $expectedBaseTotalAmount = 10; + + $this->itemMock->expects($this->once())->method('getBaseRowTotal')->willReturn(8); + $this->itemMock->expects($this->once())->method('getBaseTaxAmount')->willReturn(1); + $this->itemMock->expects($this->once())->method('getBaseDiscountTaxCompensationAmount')->willReturn(1); + $this->itemMock->expects($this->once())->method('getBaseWeeeTaxAppliedAmount')->willReturn(1); + $this->itemMock->expects($this->once())->method('getBaseDiscountAmount')->willReturn(1); + + $this->assertEquals($expectedBaseTotalAmount, $this->block->getBaseTotalAmount($this->itemMock)); + } } diff --git a/app/code/Magento/Sales/Test/Unit/Controller/Adminhtml/Order/AddCommentTest.php b/app/code/Magento/Sales/Test/Unit/Controller/Adminhtml/Order/AddCommentTest.php new file mode 100644 index 0000000000000..d72121878e350 --- /dev/null +++ b/app/code/Magento/Sales/Test/Unit/Controller/Adminhtml/Order/AddCommentTest.php @@ -0,0 +1,186 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Sales\Test\Unit\Controller\Adminhtml\Order; + +/** + * Test for AddComment. + */ +class AddCommentTest extends \PHPUnit\Framework\TestCase +{ + /** + * @var \Magento\Sales\Controller\Adminhtml\Order\AddComment + */ + private $addCommentController; + + /** + * @var \Magento\Backend\App\Action\Context|\PHPUnit_Framework_MockObject_MockObject + */ + private $contextMock; + + /** + * @var \Magento\Sales\Model\Order|\PHPUnit_Framework_MockObject_MockObject + */ + private $orderMock; + + /** + * @var \Magento\Backend\Model\View\Result\RedirectFactory|\PHPUnit_Framework_MockObject_MockObject + */ + private $resultRedirectFactoryMock; + + /** + * @var \Magento\Backend\Model\View\Result\Redirect|\PHPUnit_Framework_MockObject_MockObject + */ + private $resultRedirectMock; + + /** + * @var \Magento\Framework\App\Request\Http|\PHPUnit_Framework_MockObject_MockObject + */ + private $requestMock; + + /** + * @var \Magento\Sales\Api\OrderRepositoryInterface|\PHPUnit_Framework_MockObject_MockObject + */ + private $orderRepositoryMock; + + /** + * @var \Magento\Framework\AuthorizationInterface|\PHPUnit_Framework_MockObject_MockObject + */ + private $authorizationMock; + + /** + * @var \Magento\Sales\Model\Order\Status\History|\PHPUnit_Framework_MockObject_MockObject + */ + private $statusHistoryCommentMock; + + /** + * @var \Magento\Framework\ObjectManagerInterface|\PHPUnit_Framework_MockObject_MockObject + */ + private $objectManagerMock; + + /** + * @inheritdoc + */ + protected function setUp() + { + $this->contextMock = $this->createMock(\Magento\Backend\App\Action\Context::class); + $this->requestMock = $this->createMock(\Magento\Framework\App\Request\Http::class); + $this->orderRepositoryMock = $this->createMock(\Magento\Sales\Api\OrderRepositoryInterface::class); + $this->orderMock = $this->createMock(\Magento\Sales\Model\Order::class); + $this->resultRedirectFactoryMock = $this->createMock(\Magento\Backend\Model\View\Result\RedirectFactory::class); + $this->resultRedirectMock = $this->createMock(\Magento\Backend\Model\View\Result\Redirect::class); + $this->authorizationMock = $this->createMock(\Magento\Framework\AuthorizationInterface::class); + $this->statusHistoryCommentMock = $this->createMock(\Magento\Sales\Model\Order\Status\History::class); + $this->objectManagerMock = $this->createMock(\Magento\Framework\ObjectManagerInterface::class); + + $this->contextMock->expects($this->once())->method('getRequest')->willReturn($this->requestMock); + + $objectManagerHelper = new \Magento\Framework\TestFramework\Unit\Helper\ObjectManager($this); + $this->addCommentController = $objectManagerHelper->getObject( + \Magento\Sales\Controller\Adminhtml\Order\AddComment::class, + [ + 'context' => $this->contextMock, + 'orderRepository' => $this->orderRepositoryMock, + '_authorization' => $this->authorizationMock, + '_objectManager' => $this->objectManagerMock, + ] + ); + } + + /** + * Test for execute method with different data. + * + * @param array $historyData + * @param bool $userHasResource + * @param bool $expectedNotify + * + * @return void + * @dataProvider executeWillNotifyCustomerDataProvider + */ + public function testExecuteWillNotifyCustomer(array $historyData, bool $userHasResource, bool $expectedNotify) + { + $orderId = 30; + $this->requestMock->expects($this->once())->method('getParam')->with('order_id')->willReturn($orderId); + $this->orderRepositoryMock->expects($this->once()) + ->method('get') + ->willReturn($this->orderMock); + $this->requestMock->expects($this->once())->method('getPost')->with('history')->willReturn($historyData); + $this->authorizationMock->expects($this->any())->method('isAllowed')->willReturn($userHasResource); + $this->orderMock->expects($this->once()) + ->method('addStatusHistoryComment') + ->willReturn($this->statusHistoryCommentMock); + $this->statusHistoryCommentMock->expects($this->once())->method('setIsCustomerNotified')->with($expectedNotify); + $this->objectManagerMock->expects($this->once())->method('create')->willReturn( + $this->createMock(\Magento\Sales\Model\Order\Email\Sender\OrderCommentSender::class) + ); + + $this->addCommentController->execute(); + } + + /** + * Data provider for testExecuteWillNotifyCustomer method. + * + * @return array + */ + public function executeWillNotifyCustomerDataProvider(): array + { + return [ + 'User Has Access - Notify True' => [ + 'postData' => [ + 'comment' => 'Great Product!', + 'is_customer_notified' => true, + 'status' => 'Processing', + ], + 'userHasResource' => true, + 'expectedNotify' => true, + ], + 'User Has Access - Notify False' => [ + 'postData' => [ + 'comment' => 'Great Product!', + 'is_customer_notified' => false, + 'status' => 'Processing', + ], + 'userHasResource' => true, + 'expectedNotify' => false, + ], + 'User Has Access - Notify Unset' => [ + 'postData' => [ + 'comment' => 'Great Product!', + 'status' => 'Processing', + ], + 'userHasResource' => true, + 'expectedNotify' => false, + ], + 'User No Access - Notify True' => [ + 'postData' => [ + 'comment' => 'Great Product!', + 'is_customer_notified' => true, + 'status' => 'Processing', + ], + 'userHasResource' => false, + 'expectedNotify' => false, + ], + 'User No Access - Notify False' => [ + 'postData' => [ + 'comment' => 'Great Product!', + 'is_customer_notified' => false, + 'status' => 'Processing', + ], + 'userHasResource' => false, + 'expectedNotify' => false, + ], + 'User No Access - Notify Unset' => [ + 'postData' => [ + 'comment' => 'Great Product!', + 'status' => 'Processing', + ], + 'userHasResource' => false, + 'expectedNotify' => false, + ], + ]; + } +} diff --git a/app/code/Magento/Sales/Test/Unit/Model/CronJob/CleanExpiredOrdersTest.php b/app/code/Magento/Sales/Test/Unit/Model/CronJob/CleanExpiredOrdersTest.php index 269ce829e64d3..6844b908ea98d 100644 --- a/app/code/Magento/Sales/Test/Unit/Model/CronJob/CleanExpiredOrdersTest.php +++ b/app/code/Magento/Sales/Test/Unit/Model/CronJob/CleanExpiredOrdersTest.php @@ -26,6 +26,11 @@ class CleanExpiredOrdersTest extends \PHPUnit\Framework\TestCase */ protected $orderCollectionMock; + /** + * @var \PHPUnit_Framework_MockObject_MockObject + */ + private $orderManagementMock; + /** * @var ObjectManager */ @@ -44,10 +49,12 @@ protected function setUp() ['create'] ); $this->orderCollectionMock = $this->createMock(\Magento\Sales\Model\ResourceModel\Order\Collection::class); + $this->orderManagementMock = $this->createMock(\Magento\Sales\Api\OrderManagementInterface::class); $this->model = new CleanExpiredOrders( $this->storesConfigMock, - $this->collectionFactoryMock + $this->collectionFactoryMock, + $this->orderManagementMock ); } @@ -64,8 +71,11 @@ public function testExecute() $this->collectionFactoryMock->expects($this->exactly(2)) ->method('create') ->willReturn($this->orderCollectionMock); + $this->orderCollectionMock->expects($this->exactly(2)) + ->method('getAllIds') + ->willReturn([1, 2]); $this->orderCollectionMock->expects($this->exactly(4))->method('addFieldToFilter'); - $this->orderCollectionMock->expects($this->exactly(4))->method('walk'); + $this->orderManagementMock->expects($this->exactly(4))->method('cancel'); $selectMock = $this->createMock(\Magento\Framework\DB\Select::class); $selectMock->expects($this->exactly(2))->method('where')->willReturnSelf(); @@ -92,14 +102,18 @@ public function testExecuteWithException() $this->collectionFactoryMock->expects($this->once()) ->method('create') ->willReturn($this->orderCollectionMock); + $this->orderCollectionMock->expects($this->once()) + ->method('getAllIds') + ->willReturn([1]); $this->orderCollectionMock->expects($this->exactly(2))->method('addFieldToFilter'); + $this->orderManagementMock->expects($this->once())->method('cancel'); $selectMock = $this->createMock(\Magento\Framework\DB\Select::class); $selectMock->expects($this->once())->method('where')->willReturnSelf(); $this->orderCollectionMock->expects($this->once())->method('getSelect')->willReturn($selectMock); - $this->orderCollectionMock->expects($this->once()) - ->method('walk') + $this->orderManagementMock->expects($this->once()) + ->method('cancel') ->willThrowException(new \Exception($exceptionMessage)); $this->model->execute(); diff --git a/app/code/Magento/Sales/Test/Unit/Model/Order/Creditmemo/Total/TaxTest.php b/app/code/Magento/Sales/Test/Unit/Model/Order/Creditmemo/Total/TaxTest.php index 01c0565a557a6..f9a5a428bb554 100644 --- a/app/code/Magento/Sales/Test/Unit/Model/Order/Creditmemo/Total/TaxTest.php +++ b/app/code/Magento/Sales/Test/Unit/Model/Order/Creditmemo/Total/TaxTest.php @@ -440,10 +440,10 @@ public function collectDataProvider() ], ], 'creditmemo_data' => [ - 'grand_total' => 64.95, - 'base_grand_total' => 64.95, - 'tax_amount' => 4.95, - 'base_tax_amount' => 4.95, + 'grand_total' => 64.94, + 'base_grand_total' => 64.94, + 'tax_amount' => 4.94, + 'base_tax_amount' => 4.94, ], ], ]; diff --git a/app/code/Magento/Sales/Test/Unit/Model/Order/Webapi/ChangeOutputArrayTest.php b/app/code/Magento/Sales/Test/Unit/Model/Order/Webapi/ChangeOutputArrayTest.php new file mode 100644 index 0000000000000..83c40707c0079 --- /dev/null +++ b/app/code/Magento/Sales/Test/Unit/Model/Order/Webapi/ChangeOutputArrayTest.php @@ -0,0 +1,92 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Sales\Test\Unit\Model\Order\Webapi; + +use Magento\Sales\Api\Data\OrderItemInterface; +use Magento\Sales\Block\Adminhtml\Items\Column\DefaultColumn; +use Magento\Framework\TestFramework\Unit\Helper\ObjectManager; +use Magento\Sales\Block\Order\Item\Renderer\DefaultRenderer; +use Magento\Sales\Model\Order\Webapi\ChangeOutputArray; + +/** + * Test for Magento\Sales\Model\Order\Webapi\ChangeOutputArray class. + */ +class ChangeOutputArrayTest extends \PHPUnit\Framework\TestCase +{ + /** + * @var DefaultColumn|\PHPUnit_Framework_MockObject_MockObject + */ + private $priceRendererMock; + + /** + * @var DefaultRenderer|\PHPUnit_Framework_MockObject_MockObject + */ + private $defaultRendererMock; + + /** + * @var ObjectManager + */ + private $objectManager; + + /** + * @var ChangeOutputArray + */ + private $model; + + /** + * @inheritdoc + */ + protected function setUp() + { + $this->objectManager = new ObjectManager($this); + + $this->priceRendererMock = $this->createMock(DefaultColumn::class); + $this->defaultRendererMock = $this->createMock(DefaultRenderer::class); + + $this->model = $this->objectManager->getObject( + ChangeOutputArray::class, + [ + 'priceRenderer' => $this->priceRendererMock, + 'defaultRenderer' => $this->defaultRendererMock, + ] + ); + } + + /** + * @return void + */ + public function testExecute() + { + $expectedResult = [ + OrderItemInterface::ROW_TOTAL => 10, + OrderItemInterface::BASE_ROW_TOTAL => 10, + OrderItemInterface::ROW_TOTAL_INCL_TAX => 11, + OrderItemInterface::BASE_ROW_TOTAL_INCL_TAX => 11, + ]; + $orderItemInterfaceMock = $this->createMock(OrderItemInterface::class); + + $this->priceRendererMock->expects($this->once()) + ->method('getTotalAmount') + ->with($orderItemInterfaceMock) + ->willReturn(10); + $this->priceRendererMock->expects($this->once()) + ->method('getBaseTotalAmount') + ->with($orderItemInterfaceMock) + ->willReturn(10); + $this->defaultRendererMock->expects($this->once()) + ->method('getTotalAmount') + ->with($orderItemInterfaceMock) + ->willReturn(11); + $this->defaultRendererMock->expects($this->once()) + ->method('getBaseTotalAmount') + ->with($orderItemInterfaceMock) + ->willReturn(11); + + $this->assertEquals($expectedResult, $this->model->execute($orderItemInterfaceMock, [])); + } +} diff --git a/app/code/Magento/Sales/Test/Unit/Model/OrderRepositoryTest.php b/app/code/Magento/Sales/Test/Unit/Model/OrderRepositoryTest.php index 3df667094f2a9..04b774c8a74fd 100644 --- a/app/code/Magento/Sales/Test/Unit/Model/OrderRepositoryTest.php +++ b/app/code/Magento/Sales/Test/Unit/Model/OrderRepositoryTest.php @@ -9,6 +9,8 @@ use Magento\Sales\Api\Data\OrderInterface; use Magento\Sales\Api\Data\OrderSearchResultInterfaceFactory as SearchResultFactory; use Magento\Sales\Model\ResourceModel\Metadata; +use Magento\Tax\Api\OrderTaxManagementInterface; +use Magento\Payment\Api\Data\PaymentAdditionalInfoInterfaceFactory; /** * @SuppressWarnings(PHPMD.CouplingBetweenObjects) @@ -41,7 +43,17 @@ class OrderRepositoryTest extends \PHPUnit\Framework\TestCase private $collectionProcessor; /** - * Setup the test + * @var OrderTaxManagementInterface|\PHPUnit_Framework_MockObject_MockObject + */ + private $orderTaxManagementMock; + + /** + * @var PaymentAdditionalInfoInterfaceFactory|\PHPUnit_Framework_MockObject_MockObject + */ + private $paymentAdditionalInfoFactory; + + /** + * @inheritdoc */ protected function setUp() { @@ -58,34 +70,67 @@ protected function setUp() $orderExtensionFactoryMock = $this->getMockBuilder(\Magento\Sales\Api\Data\OrderExtensionFactory::class) ->disableOriginalConstructor() ->getMock(); + $this->orderTaxManagementMock = $this->getMockBuilder(OrderTaxManagementInterface::class) + ->disableOriginalConstructor() + ->getMockForAbstractClass(); + $this->paymentAdditionalInfoFactory = $this->getMockBuilder(PaymentAdditionalInfoInterfaceFactory::class) + ->disableOriginalConstructor()->setMethods(['create'])->getMockForAbstractClass(); $this->orderRepository = $this->objectManager->getObject( \Magento\Sales\Model\OrderRepository::class, [ 'metadata' => $this->metadata, 'searchResultFactory' => $this->searchResultFactory, 'collectionProcessor' => $this->collectionProcessor, - 'orderExtensionFactory' => $orderExtensionFactoryMock + 'orderExtensionFactory' => $orderExtensionFactoryMock, + 'orderTaxManagement' => $this->orderTaxManagementMock, + 'paymentAdditionalInfoFactory' => $this->paymentAdditionalInfoFactory, ] ); } + /** + * Test for method getList. + * + * @return void + */ public function testGetList() { $searchCriteriaMock = $this->createMock(\Magento\Framework\Api\SearchCriteria::class); $collectionMock = $this->createMock(\Magento\Sales\Model\ResourceModel\Order\Collection::class); - $itemsMock = $this->getMockBuilder(OrderInterface::class)->disableOriginalConstructor()->getMock(); + $itemsMock = $this->getMockBuilder(OrderInterface::class)->disableOriginalConstructor() + ->getMockForAbstractClass(); + $orderTaxDetailsMock = $this->getMockBuilder(\Magento\Tax\Api\Data\OrderTaxDetailsInterface::class) + ->disableOriginalConstructor() + ->setMethods(['getAppliedTaxes', 'getItems'])->getMockForAbstractClass(); + $paymentMock = $this->getMockBuilder(\Magento\Sales\Api\Data\OrderPaymentInterface::class) + ->disableOriginalConstructor()->getMockForAbstractClass(); + $paymentAdditionalInfo = $this->getMockBuilder(\Magento\Payment\Api\Data\PaymentAdditionalInfoInterface::class) + ->disableOriginalConstructor()->setMethods(['setKey', 'setValue'])->getMockForAbstractClass(); $extensionAttributes = $this->createPartialMock( \Magento\Sales\Api\Data\OrderExtension::class, - ['getShippingAssignments'] + [ + 'getShippingAssignments', 'setShippingAssignments', 'setConvertingFromQuote', + 'setAppliedTaxes', 'setItemAppliedTaxes', 'setPaymentAdditionalInfo', + ] ); $shippingAssignmentBuilder = $this->createMock( \Magento\Sales\Model\Order\ShippingAssignmentBuilder::class ); + $itemsMock->expects($this->atLeastOnce())->method('getEntityId')->willReturn(1); $this->collectionProcessor->expects($this->once()) ->method('process') ->with($searchCriteriaMock, $collectionMock); - $itemsMock->expects($this->once())->method('getExtensionAttributes')->willReturn($extensionAttributes); + $itemsMock->expects($this->atLeastOnce())->method('getExtensionAttributes')->willReturn($extensionAttributes); + $itemsMock->expects($this->atleastOnce())->method('getPayment')->willReturn($paymentMock); + $paymentMock->expects($this->atLeastOnce())->method('getAdditionalInformation') + ->willReturn(['method' => 'checkmo']); + $this->paymentAdditionalInfoFactory->expects($this->atLeastOnce())->method('create') + ->willReturn($paymentAdditionalInfo); + $paymentAdditionalInfo->expects($this->atLeastOnce())->method('setKey')->willReturnSelf(); + $paymentAdditionalInfo->expects($this->atLeastOnce())->method('setValue')->willReturnSelf(); + $this->orderTaxManagementMock->expects($this->atLeastOnce())->method('getOrderTaxDetails') + ->willReturn($orderTaxDetailsMock); $extensionAttributes->expects($this->any()) ->method('getShippingAssignments') ->willReturn($shippingAssignmentBuilder); @@ -96,6 +141,11 @@ public function testGetList() $this->assertEquals($collectionMock, $this->orderRepository->getList($searchCriteriaMock)); } + /** + * Test for method save. + * + * @return void + */ public function testSave() { $mapperMock = $this->getMockBuilder(\Magento\Sales\Model\ResourceModel\Order::class) diff --git a/app/code/Magento/Sales/Test/Unit/Model/ResourceModel/Order/Handler/StateTest.php b/app/code/Magento/Sales/Test/Unit/Model/ResourceModel/Order/Handler/StateTest.php index e120d613e323c..48f4a282a2be2 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 @@ -8,7 +8,7 @@ use Magento\Sales\Model\Order; /** - * Class StateTest + * Tests for State. */ class StateTest extends \PHPUnit\Framework\TestCase { @@ -22,9 +22,14 @@ class StateTest extends \PHPUnit\Framework\TestCase */ protected $orderMock; + /** + * @inheritdoc + */ protected function setUp() { - $this->orderMock = $this->createPartialMock(\Magento\Sales\Model\Order::class, [ + $this->orderMock = $this->createPartialMock( + \Magento\Sales\Model\Order::class, + [ '__wakeup', 'getId', 'hasCustomerNoteNotify', @@ -35,13 +40,12 @@ protected function setUp() 'canShip', 'getBaseGrandTotal', 'canCreditmemo', - 'getState', - 'setState', 'getTotalRefunded', 'hasForcedCanCreditmemo', 'getIsInProcess', 'getConfig', - ]); + ] + ); $this->orderMock->expects($this->any()) ->method('getConfig') ->willReturnSelf(); @@ -53,127 +57,96 @@ protected function setUp() } /** - * test check order - order without id + * Test for check method with different states. + * + * @param bool $isCanceled + * @param bool $canUnhold + * @param bool $canInvoice + * @param bool $canShip + * @param int $callCanSkipNum + * @param bool $canCreditmemo + * @param int $callCanCreditmemoNum + * @param string $currentState + * @param string $expectedState + * @param int $callSetStateNum + * @return void + * @dataProvider stateCheckDataProvider + * @SuppressWarnings(PHPMD.ExcessiveParameterList) */ - public function testCheckOrderEmpty() - { - $this->orderMock->expects($this->once()) - ->method('getBaseGrandTotal') - ->willReturn(100); - $this->orderMock->expects($this->never()) - ->method('setState'); - - $this->state->check($this->orderMock); - } - - /** - * test check order - set state complete - */ - public function testCheckSetStateComplete() - { + public function testCheck( + bool $canCreditmemo, + int $callCanCreditmemoNum, + bool $canShip, + int $callCanSkipNum, + string $currentState, + string $expectedState = '', + bool $isInProcess = false, + int $callGetIsInProcessNum = 0, + bool $isCanceled = false, + bool $canUnhold = false, + bool $canInvoice = false + ) { + $this->orderMock->setState($currentState); $this->orderMock->expects($this->any()) - ->method('getId') - ->will($this->returnValue(1)); - $this->orderMock->expects($this->once()) ->method('isCanceled') - ->will($this->returnValue(false)); - $this->orderMock->expects($this->once()) - ->method('canUnhold') - ->will($this->returnValue(false)); - $this->orderMock->expects($this->once()) - ->method('canInvoice') - ->will($this->returnValue(false)); - $this->orderMock->expects($this->once()) - ->method('canShip') - ->will($this->returnValue(false)); - $this->orderMock->expects($this->once()) - ->method('getBaseGrandTotal') - ->will($this->returnValue(100)); - $this->orderMock->expects($this->once()) - ->method('canCreditmemo') - ->will($this->returnValue(true)); - $this->orderMock->expects($this->exactly(2)) - ->method('getState') - ->will($this->returnValue(Order::STATE_PROCESSING)); - $this->orderMock->expects($this->once()) - ->method('setState') - ->with(Order::STATE_COMPLETE) - ->will($this->returnSelf()); - $this->assertEquals($this->state, $this->state->check($this->orderMock)); - } - - /** - * test check order - set state closed - */ - public function testCheckSetStateClosed() - { + ->willReturn($isCanceled); $this->orderMock->expects($this->any()) - ->method('getId') - ->will($this->returnValue(1)); - $this->orderMock->expects($this->once()) - ->method('isCanceled') - ->will($this->returnValue(false)); - $this->orderMock->expects($this->once()) ->method('canUnhold') - ->will($this->returnValue(false)); - $this->orderMock->expects($this->once()) + ->willReturn($canUnhold); + $this->orderMock->expects($this->any()) ->method('canInvoice') - ->will($this->returnValue(false)); - $this->orderMock->expects($this->once()) + ->willReturn($canInvoice); + $this->orderMock->expects($this->exactly($callCanSkipNum)) ->method('canShip') - ->will($this->returnValue(false)); - $this->orderMock->expects($this->once()) - ->method('getBaseGrandTotal') - ->will($this->returnValue(100)); - $this->orderMock->expects($this->once()) + ->willReturn($canShip); + $this->orderMock->expects($this->exactly($callCanCreditmemoNum)) ->method('canCreditmemo') - ->will($this->returnValue(false)); - $this->orderMock->expects($this->exactly(2)) - ->method('getTotalRefunded') - ->will($this->returnValue(null)); - $this->orderMock->expects($this->once()) - ->method('hasForcedCanCreditmemo') - ->will($this->returnValue(true)); - $this->orderMock->expects($this->exactly(2)) - ->method('getState') - ->will($this->returnValue(Order::STATE_PROCESSING)); - $this->orderMock->expects($this->once()) - ->method('setState') - ->with(Order::STATE_CLOSED) - ->will($this->returnSelf()); - $this->assertEquals($this->state, $this->state->check($this->orderMock)); + ->willReturn($canCreditmemo); + $this->orderMock->expects($this->exactly($callGetIsInProcessNum)) + ->method('getIsInProcess') + ->willReturn($isInProcess); + $this->state->check($this->orderMock); + $this->assertEquals($expectedState, $this->orderMock->getState()); } /** - * test check order - set state processing + * Data provider for testCheck method. + * + * @return array */ - public function testCheckSetStateProcessing() + public function stateCheckDataProvider(): array { - $this->orderMock->expects($this->any()) - ->method('getId') - ->will($this->returnValue(1)); - $this->orderMock->expects($this->once()) - ->method('isCanceled') - ->will($this->returnValue(false)); - $this->orderMock->expects($this->once()) - ->method('canUnhold') - ->will($this->returnValue(false)); - $this->orderMock->expects($this->once()) - ->method('canInvoice') - ->will($this->returnValue(false)); - $this->orderMock->expects($this->once()) - ->method('canShip') - ->will($this->returnValue(true)); - $this->orderMock->expects($this->once()) - ->method('getState') - ->will($this->returnValue(Order::STATE_NEW)); - $this->orderMock->expects($this->once()) - ->method('getIsInProcess') - ->will($this->returnValue(true)); - $this->orderMock->expects($this->once()) - ->method('setState') - ->with(Order::STATE_PROCESSING) - ->will($this->returnSelf()); - $this->assertEquals($this->state, $this->state->check($this->orderMock)); + return [ + 'processing - !canCreditmemo!canShip -> closed' => + [false, 1, false, 0, Order::STATE_PROCESSING, Order::STATE_CLOSED], + 'complete - !canCreditmemo,!canShip -> closed' => + [false, 1, false, 0, Order::STATE_COMPLETE, Order::STATE_CLOSED], + 'processing - !canCreditmemo,canShip -> closed' => + [false, 1, true, 0, Order::STATE_PROCESSING, Order::STATE_CLOSED], + 'complete - !canCreditmemo,canShip -> closed' => + [false, 1, true, 0, Order::STATE_COMPLETE, Order::STATE_CLOSED], + 'processing - canCreditmemo,!canShip -> complete' => + [true, 1, false, 1, Order::STATE_PROCESSING, Order::STATE_COMPLETE], + 'complete - canCreditmemo,!canShip -> complete' => + [true, 1, false, 0, Order::STATE_COMPLETE, Order::STATE_COMPLETE], + 'processing - canCreditmemo, canShip -> processing' => + [true, 1, true, 1, Order::STATE_PROCESSING, Order::STATE_PROCESSING], + 'complete - canCreditmemo, canShip -> complete' => + [true, 1, true, 0, Order::STATE_COMPLETE, Order::STATE_COMPLETE], + 'new - canCreditmemo, canShip, IsInProcess -> processing' => + [true, 1, true, 1, Order::STATE_NEW, Order::STATE_PROCESSING, true, 1], + 'new - canCreditmemo, !canShip, IsInProcess -> processing' => + [true, 1, false, 1, Order::STATE_NEW, Order::STATE_COMPLETE, true, 1], + 'new - canCreditmemo, canShip, !IsInProcess -> new' => + [true, 0, true, 0, Order::STATE_NEW, Order::STATE_NEW, false, 1], + 'hold - canUnhold -> hold' => + [true, 0, true, 0, Order::STATE_HOLDED, Order::STATE_HOLDED, false, 0, false, true], + 'payment_review - canUnhold -> payment_review' => + [true, 0, true, 0, Order::STATE_PAYMENT_REVIEW, Order::STATE_PAYMENT_REVIEW, false, 0, false, true], + 'pending_payment - canUnhold -> pending_payment' => + [true, 0, true, 0, Order::STATE_PENDING_PAYMENT, Order::STATE_PENDING_PAYMENT, false, 0, false, true], + 'cancelled - isCanceled -> cancelled' => + [true, 0, true, 0, Order::STATE_HOLDED, Order::STATE_HOLDED, false, 0, true], + ]; } } diff --git a/app/code/Magento/Sales/Test/Unit/Observer/AssignOrderToCustomerObserverTest.php b/app/code/Magento/Sales/Test/Unit/Observer/AssignOrderToCustomerObserverTest.php index c6e02151b9bc1..18371274049e3 100644 --- a/app/code/Magento/Sales/Test/Unit/Observer/AssignOrderToCustomerObserverTest.php +++ b/app/code/Magento/Sales/Test/Unit/Observer/AssignOrderToCustomerObserverTest.php @@ -12,6 +12,7 @@ use Magento\Framework\Event\Observer; use Magento\Sales\Api\Data\OrderInterface; use Magento\Sales\Api\OrderRepositoryInterface; +use Magento\Sales\Model\Order\CustomerAssignment; use Magento\Sales\Observer\AssignOrderToCustomerObserver; use PHPUnit\Framework\TestCase; use PHPUnit_Framework_MockObject_MockObject; @@ -27,6 +28,9 @@ class AssignOrderToCustomerObserverTest extends TestCase /** @var OrderRepositoryInterface|PHPUnit_Framework_MockObject_MockObject */ protected $orderRepositoryMock; + /** @var CustomerAssignment | PHPUnit_Framework_MockObject_MockObject */ + protected $assignmentMock; + /** * Set Up */ @@ -35,7 +39,12 @@ protected function setUp() $this->orderRepositoryMock = $this->getMockBuilder(OrderRepositoryInterface::class) ->disableOriginalConstructor() ->getMock(); - $this->sut = new AssignOrderToCustomerObserver($this->orderRepositoryMock); + + $this->assignmentMock = $this->getMockBuilder(CustomerAssignment::class) + ->disableOriginalConstructor() + ->getMock(); + + $this->sut = new AssignOrderToCustomerObserver($this->orderRepositoryMock, $this->assignmentMock); } /** @@ -69,13 +78,14 @@ public function testAssignOrderToCustomerAfterGuestOrder($customerId) $orderMock->expects($this->once())->method('getCustomerId')->willReturn($customerId); $this->orderRepositoryMock->expects($this->once())->method('get')->with($orderId) ->willReturn($orderMock); - if (!$customerId) { - $this->orderRepositoryMock->expects($this->once())->method('save')->with($orderMock); + + if ($customerId) { + $this->assignmentMock->expects($this->once())->method('execute')->with($orderMock, $customerMock); $this->sut->execute($observerMock); - return ; + return; } - $this->orderRepositoryMock->expects($this->never())->method('save')->with($orderMock); + $this->assignmentMock->expects($this->never())->method('execute'); $this->sut->execute($observerMock); } diff --git a/app/code/Magento/Sales/ViewModel/Customer/AddressFormatter.php b/app/code/Magento/Sales/ViewModel/Customer/AddressFormatter.php new file mode 100644 index 0000000000000..16de1412a2a45 --- /dev/null +++ b/app/code/Magento/Sales/ViewModel/Customer/AddressFormatter.php @@ -0,0 +1,131 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +namespace Magento\Sales\ViewModel\Customer; + +use Magento\Framework\View\Element\Block\ArgumentInterface; + +/** + * Customer address formatter + */ +class AddressFormatter implements ArgumentInterface +{ + /** + * Customer form factory + * + * @var \Magento\Customer\Model\Metadata\FormFactory + */ + private $customerFormFactory; + + /** + * Address format helper + * + * @var \Magento\Customer\Helper\Address + */ + private $addressFormatHelper; + + /** + * Directory helper + * + * @var \Magento\Directory\Helper\Data + */ + private $directoryHelper; + + /** + * Session quote + * + * @var \Magento\Backend\Model\Session\Quote + */ + private $session; + + /** + * Json encoder + * + * @var \Magento\Framework\Serialize\Serializer\Json + */ + private $jsonEncoder; + + /** + * Customer address + * + * @param \Magento\Customer\Model\Metadata\FormFactory $customerFormFactory + * @param \Magento\Customer\Helper\Address $addressFormatHelper + * @param \Magento\Directory\Helper\Data $directoryHelper + * @param \Magento\Backend\Model\Session\Quote $session + * @param \Magento\Framework\Serialize\Serializer\Json $jsonEncoder + */ + public function __construct( + \Magento\Customer\Model\Metadata\FormFactory $customerFormFactory, + \Magento\Customer\Helper\Address $addressFormatHelper, + \Magento\Directory\Helper\Data $directoryHelper, + \Magento\Backend\Model\Session\Quote $session, + \Magento\Framework\Serialize\Serializer\Json $jsonEncoder + ) { + $this->customerFormFactory = $customerFormFactory; + $this->addressFormatHelper = $addressFormatHelper; + $this->directoryHelper = $directoryHelper; + $this->session = $session; + $this->jsonEncoder = $jsonEncoder; + } + + /** + * Return customer address array as JSON + * + * @param array $addressArray + * + * @return string + */ + public function getAddressesJson(array $addressArray) + { + $data = $this->getEmptyAddressForm(); + foreach ($addressArray as $addressId => $address) { + $addressForm = $this->customerFormFactory->create( + 'customer_address', + 'adminhtml_customer_address', + $address + ); + $data[$addressId] = $addressForm->outputData( + \Magento\Eav\Model\AttributeDataFactory::OUTPUT_FORMAT_JSON + ); + } + + return $this->jsonEncoder->serialize($data); + } + + /** + * Represent customer address in 'online' format. + * + * @param array $address + * @return string + */ + public function getAddressAsString(array $address) + { + $formatTypeRenderer = $this->addressFormatHelper->getFormatTypeRenderer('oneline'); + $result = ''; + if ($formatTypeRenderer) { + $result = $formatTypeRenderer->renderArray($address); + } + + return $result; + } + + /** + * Return empty address address form + * + * @return array + */ + private function getEmptyAddressForm() + { + $defaultCountryId = $this->directoryHelper->getDefaultCountry($this->session->getStore()); + $emptyAddressForm = $this->customerFormFactory->create( + 'customer_address', + 'adminhtml_customer_address', + [\Magento\Customer\Api\Data\AddressInterface::COUNTRY_ID => $defaultCountryId] + ); + + return [0 => $emptyAddressForm->outputData(\Magento\Eav\Model\AttributeDataFactory::OUTPUT_FORMAT_JSON)]; + } +} diff --git a/app/code/Magento/Sales/etc/di.xml b/app/code/Magento/Sales/etc/di.xml index ac7b8def08aa8..b4c1e63902121 100644 --- a/app/code/Magento/Sales/etc/di.xml +++ b/app/code/Magento/Sales/etc/di.xml @@ -746,6 +746,10 @@ <virtualType name="ShippingAddressAggregator" type="Magento\Framework\DB\Sql\ConcatExpression"> <arguments> <argument name="columns" xsi:type="array"> + <item name="company" xsi:type="array"> + <item name="tableAlias" xsi:type="string">sales_shipping_address</item> + <item name="columnName" xsi:type="string">company</item> + </item> <item name="street" xsi:type="array"> <item name="tableAlias" xsi:type="string">sales_shipping_address</item> <item name="columnName" xsi:type="string">street</item> @@ -769,6 +773,10 @@ <virtualType name="BillingAddressAggregator" type="Magento\Framework\DB\Sql\ConcatExpression"> <arguments> <argument name="columns" xsi:type="array"> + <item name="company" xsi:type="array"> + <item name="tableAlias" xsi:type="string">sales_billing_address</item> + <item name="columnName" xsi:type="string">company</item> + </item> <item name="street" xsi:type="array"> <item name="tableAlias" xsi:type="string">sales_billing_address</item> <item name="columnName" xsi:type="string">street</item> diff --git a/app/code/Magento/Sales/etc/extension_attributes.xml b/app/code/Magento/Sales/etc/extension_attributes.xml index 7280a1a071548..222f61cdc7324 100644 --- a/app/code/Magento/Sales/etc/extension_attributes.xml +++ b/app/code/Magento/Sales/etc/extension_attributes.xml @@ -10,4 +10,7 @@ <extension_attributes for="Magento\Sales\Api\Data\OrderInterface"> <attribute code="shipping_assignments" type="Magento\Sales\Api\Data\ShippingAssignmentInterface[]" /> </extension_attributes> + <extension_attributes for="Magento\Sales\Api\Data\OrderInterface"> + <attribute code="payment_additional_info" type="Magento\Payment\Api\Data\PaymentAdditionalInfoInterface[]" /> + </extension_attributes> </config> diff --git a/app/code/Magento/Sales/etc/webapi.xml b/app/code/Magento/Sales/etc/webapi.xml index cee245e348393..492dff8057039 100644 --- a/app/code/Magento/Sales/etc/webapi.xml +++ b/app/code/Magento/Sales/etc/webapi.xml @@ -10,271 +10,271 @@ <route url="/V1/orders/:id" method="GET"> <service class="Magento\Sales\Api\OrderRepositoryInterface" method="get"/> <resources> - <resource ref="Magento_Sales::sales" /> + <resource ref="Magento_Sales::actions_view" /> </resources> </route> <route url="/V1/orders" method="GET"> <service class="Magento\Sales\Api\OrderRepositoryInterface" method="getList"/> <resources> - <resource ref="Magento_Sales::sales" /> + <resource ref="Magento_Sales::actions_view" /> </resources> </route> <route url="/V1/orders/:id/statuses" method="GET"> <service class="Magento\Sales\Api\OrderManagementInterface" method="getStatus"/> <resources> - <resource ref="Magento_Sales::sales" /> + <resource ref="Magento_Sales::actions_view" /> </resources> </route> <route url="/V1/orders/:id/cancel" method="POST"> <service class="Magento\Sales\Api\OrderManagementInterface" method="cancel"/> <resources> - <resource ref="Magento_Sales::sales" /> + <resource ref="Magento_Sales::cancel" /> </resources> </route> <route url="/V1/orders/:id/emails" method="POST"> <service class="Magento\Sales\Api\OrderManagementInterface" method="notify"/> <resources> - <resource ref="Magento_Sales::sales" /> + <resource ref="Magento_Sales::emails" /> </resources> </route> <route url="/V1/orders/:id/hold" method="POST"> <service class="Magento\Sales\Api\OrderManagementInterface" method="hold"/> <resources> - <resource ref="Magento_Sales::sales" /> + <resource ref="Magento_Sales::hold" /> </resources> </route> <route url="/V1/orders/:id/unhold" method="POST"> <service class="Magento\Sales\Api\OrderManagementInterface" method="unHold"/> <resources> - <resource ref="Magento_Sales::sales" /> + <resource ref="Magento_Sales::unhold" /> </resources> </route> <route url="/V1/orders/:id/comments" method="POST"> <service class="Magento\Sales\Api\OrderManagementInterface" method="addComment"/> <resources> - <resource ref="Magento_Sales::sales" /> + <resource ref="Magento_Sales::comment" /> </resources> </route> <route url="/V1/orders/:id/comments" method="GET"> <service class="Magento\Sales\Api\OrderManagementInterface" method="getCommentsList"/> <resources> - <resource ref="Magento_Sales::sales" /> + <resource ref="Magento_Sales::actions_view" /> </resources> </route> <route url="/V1/orders/create" method="PUT"> <service class="Magento\Sales\Api\OrderRepositoryInterface" method="save"/> <resources> - <resource ref="Magento_Sales::sales" /> + <resource ref="Magento_Sales::create" /> </resources> </route> <route url="/V1/orders/:parent_id" method="PUT"> <service class="Magento\Sales\Api\OrderAddressRepositoryInterface" method="save"/> <resources> - <resource ref="Magento_Sales::sales" /> + <resource ref="Magento_Sales::create" /> </resources> </route> <route url="/V1/orders/items/:id" method="GET"> <service class="Magento\Sales\Api\OrderItemRepositoryInterface" method="get"/> <resources> - <resource ref="Magento_Sales::sales" /> + <resource ref="Magento_Sales::actions_view" /> </resources> </route> <route url="/V1/orders/items" method="GET"> <service class="Magento\Sales\Api\OrderItemRepositoryInterface" method="getList"/> <resources> - <resource ref="Magento_Sales::sales" /> + <resource ref="Magento_Sales::actions_view" /> </resources> </route> <route url="/V1/invoices/:id" method="GET"> <service class="Magento\Sales\Api\InvoiceRepositoryInterface" method="get"/> <resources> - <resource ref="Magento_Sales::sales" /> + <resource ref="Magento_Sales::sales_invoice" /> </resources> </route> <route url="/V1/invoices" method="GET"> <service class="Magento\Sales\Api\InvoiceRepositoryInterface" method="getList"/> <resources> - <resource ref="Magento_Sales::sales" /> + <resource ref="Magento_Sales::sales_invoice" /> </resources> </route> <route url="/V1/invoices/:id/comments" method="GET"> <service class="Magento\Sales\Api\InvoiceManagementInterface" method="getCommentsList"/> <resources> - <resource ref="Magento_Sales::sales" /> + <resource ref="Magento_Sales::sales_invoice" /> </resources> </route> <route url="/V1/invoices/:id/emails" method="POST"> <service class="Magento\Sales\Api\InvoiceManagementInterface" method="notify"/> <resources> - <resource ref="Magento_Sales::sales" /> + <resource ref="Magento_Sales::sales_invoice" /> </resources> </route> <route url="/V1/invoices/:id/void" method="POST"> <service class="Magento\Sales\Api\InvoiceManagementInterface" method="setVoid"/> <resources> - <resource ref="Magento_Sales::sales" /> + <resource ref="Magento_Sales::sales_invoice" /> </resources> </route> <route url="/V1/invoices/:id/capture" method="POST"> <service class="Magento\Sales\Api\InvoiceManagementInterface" method="setCapture"/> <resources> - <resource ref="Magento_Sales::sales" /> + <resource ref="Magento_Sales::sales_invoice" /> </resources> </route> <route url="/V1/invoices/comments" method="POST"> <service class="Magento\Sales\Api\InvoiceCommentRepositoryInterface" method="save"/> <resources> - <resource ref="Magento_Sales::sales" /> + <resource ref="Magento_Sales::sales_invoice" /> </resources> </route> <route url="/V1/invoices/" method="POST"> <service class="Magento\Sales\Api\InvoiceRepositoryInterface" method="save"/> <resources> - <resource ref="Magento_Sales::sales" /> + <resource ref="Magento_Sales::sales_invoice" /> </resources> </route> <route url="/V1/invoice/:invoiceId/refund" method="POST"> <service class="Magento\Sales\Api\RefundInvoiceInterface" method="execute"/> <resources> - <resource ref="Magento_Sales::sales" /> + <resource ref="Magento_Sales::sales_invoice" /> </resources> </route> <route url="/V1/creditmemo/:id/comments" method="GET"> <service class="Magento\Sales\Api\CreditmemoManagementInterface" method="getCommentsList"/> <resources> - <resource ref="Magento_Sales::sales" /> + <resource ref="Magento_Sales::sales_creditmemo" /> </resources> </route> <route url="/V1/creditmemos" method="GET"> <service class="Magento\Sales\Api\CreditmemoRepositoryInterface" method="getList"/> <resources> - <resource ref="Magento_Sales::sales" /> + <resource ref="Magento_Sales::sales_creditmemo" /> </resources> </route> <route url="/V1/creditmemo/:id" method="GET"> <service class="Magento\Sales\Api\CreditmemoRepositoryInterface" method="get"/> <resources> - <resource ref="Magento_Sales::sales" /> + <resource ref="Magento_Sales::sales_creditmemo" /> </resources> </route> <route url="/V1/creditmemo/:id" method="PUT"> <service class="Magento\Sales\Api\CreditmemoManagementInterface" method="cancel"/> <resources> - <resource ref="Magento_Sales::sales" /> + <resource ref="Magento_Sales::sales_creditmemo" /> </resources> </route> <route url="/V1/creditmemo/:id/emails" method="POST"> <service class="Magento\Sales\Api\CreditmemoManagementInterface" method="notify"/> <resources> - <resource ref="Magento_Sales::sales" /> + <resource ref="Magento_Sales::sales_creditmemo" /> </resources> </route> <route url="/V1/creditmemo/refund" method="POST"> <service class="Magento\Sales\Api\CreditmemoManagementInterface" method="refund"/> <resources> - <resource ref="Magento_Sales::sales" /> + <resource ref="Magento_Sales::sales_creditmemo" /> </resources> </route> <route url="/V1/creditmemo/:id/comments" method="POST"> <service class="Magento\Sales\Api\CreditmemoCommentRepositoryInterface" method="save"/> <resources> - <resource ref="Magento_Sales::sales" /> + <resource ref="Magento_Sales::sales_creditmemo" /> </resources> </route> <route url="/V1/creditmemo" method="POST"> <service class="Magento\Sales\Api\CreditmemoRepositoryInterface" method="save"/> <resources> - <resource ref="Magento_Sales::sales" /> + <resource ref="Magento_Sales::sales_creditmemo" /> </resources> </route> <route url="/V1/order/:orderId/refund" method="POST"> <service class="Magento\Sales\Api\RefundOrderInterface" method="execute"/> <resources> - <resource ref="Magento_Sales::sales" /> + <resource ref="Magento_Sales::creditmemo" /> </resources> </route> <route url="/V1/shipment/:id" method="GET"> <service class="Magento\Sales\Api\ShipmentRepositoryInterface" method="get"/> <resources> - <resource ref="Magento_Sales::sales" /> + <resource ref="Magento_Sales::shipment" /> </resources> </route> <route url="/V1/shipments" method="GET"> <service class="Magento\Sales\Api\ShipmentRepositoryInterface" method="getList"/> <resources> - <resource ref="Magento_Sales::sales" /> + <resource ref="Magento_Sales::shipment" /> </resources> </route> <route url="/V1/shipment/:id/comments" method="GET"> <service class="Magento\Sales\Api\ShipmentManagementInterface" method="getCommentsList"/> <resources> - <resource ref="Magento_Sales::sales" /> + <resource ref="Magento_Sales::shipment" /> </resources> </route> <route url="/V1/shipment/:id/comments" method="POST"> <service class="Magento\Sales\Api\ShipmentCommentRepositoryInterface" method="save"/> <resources> - <resource ref="Magento_Sales::sales" /> + <resource ref="Magento_Sales::shipment" /> </resources> </route> <route url="/V1/shipment/:id/emails" method="POST"> <service class="Magento\Sales\Api\ShipmentManagementInterface" method="notify"/> <resources> - <resource ref="Magento_Sales::sales" /> + <resource ref="Magento_Sales::shipment" /> </resources> </route> <route url="/V1/shipment/track" method="POST"> <service class="Magento\Sales\Api\ShipmentTrackRepositoryInterface" method="save"/> <resources> - <resource ref="Magento_Sales::sales" /> + <resource ref="Magento_Sales::shipment" /> </resources> </route> <route url="/V1/shipment/track/:id" method="DELETE"> <service class="Magento\Sales\Api\ShipmentTrackRepositoryInterface" method="deleteById"/> <resources> - <resource ref="Magento_Sales::sales" /> + <resource ref="Magento_Sales::shipment" /> </resources> </route> <route url="/V1/shipment/" method="POST"> <service class="Magento\Sales\Api\ShipmentRepositoryInterface" method="save"/> <resources> - <resource ref="Magento_Sales::sales" /> + <resource ref="Magento_Sales::shipment" /> </resources> </route> <route url="/V1/shipment/:id/label" method="GET"> <service class="Magento\Sales\Api\ShipmentManagementInterface" method="getLabel"/> <resources> - <resource ref="Magento_Sales::sales" /> + <resource ref="Magento_Sales::shipment" /> </resources> </route> <route url="/V1/order/:orderId/ship" method="POST"> <service class="Magento\Sales\Api\ShipOrderInterface" method="execute"/> <resources> - <resource ref="Magento_Sales::sales" /> + <resource ref="Magento_Sales::ship" /> </resources> </route> <route url="/V1/orders/" method="POST"> <service class="Magento\Sales\Api\OrderRepositoryInterface" method="save"/> <resources> - <resource ref="Magento_Sales::sales" /> + <resource ref="Magento_Sales::create" /> </resources> </route> <route url="/V1/transactions/:id" method="GET"> <service class="Magento\Sales\Api\TransactionRepositoryInterface" method="get"/> <resources> - <resource ref="Magento_Sales::sales" /> + <resource ref="Magento_Sales::transactions_fetch" /> </resources> </route> <route url="/V1/transactions" method="GET"> <service class="Magento\Sales\Api\TransactionRepositoryInterface" method="getList"/> <resources> - <resource ref="Magento_Sales::sales" /> + <resource ref="Magento_Sales::transactions_fetch" /> </resources> </route> <route url="/V1/order/:orderId/invoice" method="POST"> <service class="Magento\Sales\Api\InvoiceOrderInterface" method="execute"/> <resources> - <resource ref="Magento_Sales::sales" /> + <resource ref="Magento_Sales::invoice" /> </resources> </route> </routes> diff --git a/app/code/Magento/Sales/etc/webapi_rest/di.xml b/app/code/Magento/Sales/etc/webapi_rest/di.xml index 47fb3f188513c..6435445e0ef93 100644 --- a/app/code/Magento/Sales/etc/webapi_rest/di.xml +++ b/app/code/Magento/Sales/etc/webapi_rest/di.xml @@ -15,4 +15,11 @@ <type name="Magento\Sales\Api\ShipmentRepositoryInterface"> <plugin name="convert_blob_to_string" type="Magento\Sales\Plugin\ShippingLabelConverter" /> </type> + <type name="Magento\Framework\Reflection\DataObjectProcessor"> + <arguments> + <argument name="processors" xsi:type="array"> + <item name="\Magento\Sales\Model\Order\Item" xsi:type="object">Magento\Sales\Model\Order\Webapi\ChangeOutputArray\Proxy</item> + </argument> + </arguments> + </type> </config> diff --git a/app/code/Magento/Sales/etc/webapi_soap/di.xml b/app/code/Magento/Sales/etc/webapi_soap/di.xml index 47fb3f188513c..6435445e0ef93 100644 --- a/app/code/Magento/Sales/etc/webapi_soap/di.xml +++ b/app/code/Magento/Sales/etc/webapi_soap/di.xml @@ -15,4 +15,11 @@ <type name="Magento\Sales\Api\ShipmentRepositoryInterface"> <plugin name="convert_blob_to_string" type="Magento\Sales\Plugin\ShippingLabelConverter" /> </type> + <type name="Magento\Framework\Reflection\DataObjectProcessor"> + <arguments> + <argument name="processors" xsi:type="array"> + <item name="\Magento\Sales\Model\Order\Item" xsi:type="object">Magento\Sales\Model\Order\Webapi\ChangeOutputArray\Proxy</item> + </argument> + </arguments> + </type> </config> diff --git a/app/code/Magento/Sales/i18n/en_US.csv b/app/code/Magento/Sales/i18n/en_US.csv index cc18de430edc1..62d8e53e62fa0 100644 --- a/app/code/Magento/Sales/i18n/en_US.csv +++ b/app/code/Magento/Sales/i18n/en_US.csv @@ -796,3 +796,5 @@ Created,Created "PDF Creditmemos","PDF Creditmemos" Refunds,Refunds "Shipment with requested ID %1 doesn't correspond with Order with requested ID %2.","Shipment with requested ID %1 doesn't correspond with Order with requested ID %2." +"Allow Zero GrandTotal for Creditmemo","Allow Zero GrandTotal for Creditmemo" +"Allow Zero GrandTotal","Allow Zero GrandTotal" diff --git a/app/code/Magento/Sales/view/adminhtml/layout/sales_order_create_customer_block.xml b/app/code/Magento/Sales/view/adminhtml/layout/sales_order_create_customer_block.xml index c321bee460e46..0f5a3559f3008 100644 --- a/app/code/Magento/Sales/view/adminhtml/layout/sales_order_create_customer_block.xml +++ b/app/code/Magento/Sales/view/adminhtml/layout/sales_order_create_customer_block.xml @@ -80,13 +80,13 @@ <argument name="align" xsi:type="string">center</argument> </arguments> </block> - </block> - <block class="Magento\Backend\Block\Widget\Grid\Column" name="adminhtml.customer.grid.columnSet.website_name" as="website_name"> - <arguments> - <argument name="header" xsi:type="string" translate="true">Website</argument> - <argument name="index" xsi:type="string">website_name</argument> - <argument name="align" xsi:type="string">center</argument> - </arguments> + <block class="Magento\Backend\Block\Widget\Grid\Column" name="adminhtml.customer.grid.columnSet.website_name" as="website_name"> + <arguments> + <argument name="header" xsi:type="string" translate="true">Website</argument> + <argument name="index" xsi:type="string">website_name</argument> + <argument name="align" xsi:type="string">center</argument> + </arguments> + </block> </block> </block> </referenceBlock> diff --git a/app/code/Magento/Sales/view/adminhtml/layout/sales_order_create_index.xml b/app/code/Magento/Sales/view/adminhtml/layout/sales_order_create_index.xml index eb0a7685e5e22..3832476ff6972 100644 --- a/app/code/Magento/Sales/view/adminhtml/layout/sales_order_create_index.xml +++ b/app/code/Magento/Sales/view/adminhtml/layout/sales_order_create_index.xml @@ -45,8 +45,18 @@ <block class="Magento\Sales\Block\Adminhtml\Order\Create\Sidebar\Pviewed" template="Magento_Sales::order/create/sidebar/items.phtml" name="pviewed"/> </block> <block class="Magento\Sales\Block\Adminhtml\Order\Create\Form\Account" template="Magento_Sales::order/create/form/account.phtml" name="form_account"/> - <block class="Magento\Sales\Block\Adminhtml\Order\Create\Shipping\Address" template="Magento_Sales::order/create/form/address.phtml" name="shipping_address"/> - <block class="Magento\Sales\Block\Adminhtml\Order\Create\Billing\Address" template="Magento_Sales::order/create/form/address.phtml" name="billing_address"/> + <block class="Magento\Sales\Block\Adminhtml\Order\Create\Shipping\Address" template="Magento_Sales::order/create/form/address.phtml" name="shipping_address"> + <arguments> + <argument name="customerAddressFormatter" xsi:type="object">Magento\Sales\ViewModel\Customer\AddressFormatter</argument> + <argument name="customerAddressCollection" xsi:type="object">Magento\Customer\Model\ResourceModel\Address\Collection</argument> + </arguments> + </block> + <block class="Magento\Sales\Block\Adminhtml\Order\Create\Billing\Address" template="Magento_Sales::order/create/form/address.phtml" name="billing_address"> + <arguments> + <argument name="customerAddressFormatter" xsi:type="object">Magento\Sales\ViewModel\Customer\AddressFormatter</argument> + <argument name="customerAddressCollection" xsi:type="object">Magento\Customer\Model\ResourceModel\Address\Collection</argument> + </arguments> + </block> <block class="Magento\Sales\Block\Adminhtml\Order\Create\Shipping\Method" template="Magento_Sales::order/create/abstract.phtml" name="shipping_method"> <block class="Magento\Sales\Block\Adminhtml\Order\Create\Shipping\Method\Form" template="Magento_Sales::order/create/shipping/method/form.phtml" name="order_create_shipping_form" as="form"/> </block> diff --git a/app/code/Magento/Sales/view/adminhtml/layout/sales_order_create_load_block_billing_address.xml b/app/code/Magento/Sales/view/adminhtml/layout/sales_order_create_load_block_billing_address.xml index 6f0cbdb0cd43f..c52f81d5cb56d 100644 --- a/app/code/Magento/Sales/view/adminhtml/layout/sales_order_create_load_block_billing_address.xml +++ b/app/code/Magento/Sales/view/adminhtml/layout/sales_order_create_load_block_billing_address.xml @@ -8,7 +8,12 @@ <page xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:framework:View/Layout/etc/page_configuration.xsd"> <body> <referenceContainer name="content"> - <block class="Magento\Sales\Block\Adminhtml\Order\Create\Billing\Address" template="Magento_Sales::order/create/form/address.phtml" name="billing_address"/> + <block class="Magento\Sales\Block\Adminhtml\Order\Create\Billing\Address" template="Magento_Sales::order/create/form/address.phtml" name="billing_address"> + <arguments> + <argument name="customerAddressFormatter" xsi:type="object">Magento\Sales\ViewModel\Customer\AddressFormatter</argument> + <argument name="customerAddressCollection" xsi:type="object">Magento\Customer\Model\ResourceModel\Address\Collection</argument> + </arguments> + </block> </referenceContainer> </body> </page> diff --git a/app/code/Magento/Sales/view/adminhtml/layout/sales_order_create_load_block_data.xml b/app/code/Magento/Sales/view/adminhtml/layout/sales_order_create_load_block_data.xml index 70b5bfc298274..54348ce961c56 100644 --- a/app/code/Magento/Sales/view/adminhtml/layout/sales_order_create_load_block_data.xml +++ b/app/code/Magento/Sales/view/adminhtml/layout/sales_order_create_load_block_data.xml @@ -20,8 +20,18 @@ <block class="Magento\Sales\Block\Adminhtml\Order\Create\Sidebar\Pviewed" template="Magento_Sales::order/create/sidebar/items.phtml" name="pviewed"/> </block> <block class="Magento\Sales\Block\Adminhtml\Order\Create\Form\Account" template="Magento_Sales::order/create/form/account.phtml" name="form_account"/> - <block class="Magento\Sales\Block\Adminhtml\Order\Create\Shipping\Address" template="Magento_Sales::order/create/form/address.phtml" name="shipping_address"/> - <block class="Magento\Sales\Block\Adminhtml\Order\Create\Billing\Address" template="Magento_Sales::order/create/form/address.phtml" name="billing_address"/> + <block class="Magento\Sales\Block\Adminhtml\Order\Create\Shipping\Address" template="Magento_Sales::order/create/form/address.phtml" name="shipping_address"> + <arguments> + <argument name="customerAddressFormatter" xsi:type="object">Magento\Sales\ViewModel\Customer\AddressFormatter</argument> + <argument name="customerAddressCollection" xsi:type="object">Magento\Customer\Model\ResourceModel\Address\Collection</argument> + </arguments> + </block> + <block class="Magento\Sales\Block\Adminhtml\Order\Create\Billing\Address" template="Magento_Sales::order/create/form/address.phtml" name="billing_address"> + <arguments> + <argument name="customerAddressFormatter" xsi:type="object">Magento\Sales\ViewModel\Customer\AddressFormatter</argument> + <argument name="customerAddressCollection" xsi:type="object">Magento\Customer\Model\ResourceModel\Address\Collection</argument> + </arguments> + </block> <block class="Magento\Sales\Block\Adminhtml\Order\Create\Shipping\Method" template="Magento_Sales::order/create/abstract.phtml" name="shipping_method"> <block class="Magento\Sales\Block\Adminhtml\Order\Create\Shipping\Method\Form" template="Magento_Sales::order/create/shipping/method/form.phtml" name="order.create.shipping.method.form" as="form"/> </block> diff --git a/app/code/Magento/Sales/view/adminhtml/layout/sales_order_create_load_block_shipping_address.xml b/app/code/Magento/Sales/view/adminhtml/layout/sales_order_create_load_block_shipping_address.xml index 56f6786397df9..559f56dcb845b 100644 --- a/app/code/Magento/Sales/view/adminhtml/layout/sales_order_create_load_block_shipping_address.xml +++ b/app/code/Magento/Sales/view/adminhtml/layout/sales_order_create_load_block_shipping_address.xml @@ -8,7 +8,12 @@ <page xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:framework:View/Layout/etc/page_configuration.xsd"> <body> <referenceContainer name="content"> - <block class="Magento\Sales\Block\Adminhtml\Order\Create\Shipping\Address" template="Magento_Sales::order/create/form/address.phtml" name="shipping_address"/> + <block class="Magento\Sales\Block\Adminhtml\Order\Create\Shipping\Address" template="Magento_Sales::order/create/form/address.phtml" name="shipping_address"> + <arguments> + <argument name="customerAddressFormatter" xsi:type="object">Magento\Sales\ViewModel\Customer\AddressFormatter</argument> + <argument name="customerAddressCollection" xsi:type="object">Magento\Customer\Model\ResourceModel\Address\Collection</argument> + </arguments> + </block> </referenceContainer> </body> </page> diff --git a/app/code/Magento/Sales/view/adminhtml/templates/items/column/name.phtml b/app/code/Magento/Sales/view/adminhtml/templates/items/column/name.phtml index ebdf79fe7f008..98cc0d1d0ad07 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 @@ -28,7 +28,7 @@ <dt><?= $block->escapeHtml($_option['label']) ?>:</dt> <dd> <?php if (isset($_option['custom_view']) && $_option['custom_view']): ?> - <?= $block->escapeHtml($block->getCustomizedOptionValue($_option)) ?> + <?= /* @escapeNotVerified */ $block->getCustomizedOptionValue($_option) ?> <?php else: ?> <?php $_option = $block->getFormattedOption($_option['value']); ?> <?= $block->escapeHtml($_option['value']) ?> 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 686e311292ac7..b0a88b8fa37dc 100644 --- a/app/code/Magento/Sales/view/adminhtml/templates/order/create/form/address.phtml +++ b/app/code/Magento/Sales/view/adminhtml/templates/order/create/form/address.phtml @@ -6,6 +6,21 @@ // @codingStandardsIgnoreFile +/** + * @var \Magento\Customer\Model\ResourceModel\Address\Collection $addressCollection + */ +$addressCollection = $block->getData('customerAddressCollection'); + +$addressArray = []; +if ($block->getCustomerId()) { + $addressArray = $addressCollection->setCustomerFilter([$block->getCustomerId()])->toArray(); +} + +/** + * @var \Magento\Sales\ViewModel\Customer\AddressFormatter $customerAddressFormatter + */ +$customerAddressFormatter = $block->getData('customerAddressFormatter'); + /** * @var \Magento\Sales\Block\Adminhtml\Order\Create\Billing\Address|\Magento\Sales\Block\Adminhtml\Order\Create\Shipping\Address $block */ @@ -17,7 +32,7 @@ if ($block->getIsShipping()): require(["Magento_Sales/order/create/form"], function(){ order.shippingAddressContainer = '<?= /* @escapeNotVerified */ $_fieldsContainerId ?>'; - order.setAddresses(<?= /* @escapeNotVerified */ $block->getAddressCollectionJson() ?>); + order.setAddresses(<?= /* @escapeNotVerified */ $customerAddressFormatter->getAddressesJson($addressArray) ?>); }); </script> @@ -59,13 +74,11 @@ endif; ?> onchange="order.selectAddress(this, '<?= /* @escapeNotVerified */ $_fieldsContainerId ?>')" class="admin__control-select"> <option value=""><?= /* @escapeNotVerified */ __('Add New Address') ?></option> - <?php foreach ($block->getAddressCollection() as $_address): ?> - <?php //if($block->getAddressAsString($_address)!=$block->getAddressAsString($block->getAddress())): ?> + <?php foreach ($addressArray as $addressId => $address): ?> <option - value="<?= /* @escapeNotVerified */ $_address->getId() ?>"<?php if ($_address->getId() == $block->getAddressId()): ?> selected="selected"<?php endif; ?>> - <?= /* @escapeNotVerified */ $block->getAddressAsString($_address) ?> + value="<?= /* @escapeNotVerified */ $addressId ?>"<?php if ($addressId == $block->getAddressId()): ?> selected="selected"<?php endif; ?>> + <?= /* @escapeNotVerified */ $block->escapeHtml($customerAddressFormatter->getAddressAsString($address)) ?> </option> - <?php //endif; ?> <?php endforeach; ?> </select> </div> 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 fa5ea0568011b..4a77c3b166de9 100644 --- a/app/code/Magento/Sales/view/adminhtml/templates/order/invoice/create/items.phtml +++ b/app/code/Magento/Sales/view/adminhtml/templates/order/invoice/create/items.phtml @@ -31,9 +31,9 @@ <?php if ($block->canEditQty()): ?> <tfoot> <tr> - <td colspan="2"> </td> - <td colspan="3"><?= $block->getUpdateButtonHtml() ?></td> <td colspan="3"> </td> + <td><?= $block->getUpdateButtonHtml() ?></td> + <td colspan="4"> </td> </tr> </tfoot> <?php endif; ?> diff --git a/app/code/Magento/Sales/view/adminhtml/templates/order/view/info.phtml b/app/code/Magento/Sales/view/adminhtml/templates/order/view/info.phtml index 5384a00dc894d..bbd6394097f9e 100644 --- a/app/code/Magento/Sales/view/adminhtml/templates/order/view/info.phtml +++ b/app/code/Magento/Sales/view/adminhtml/templates/order/view/info.phtml @@ -104,7 +104,7 @@ $customerUrl = $block->getCustomerViewUrl(); <?php if ($order->getBaseCurrencyCode() != $order->getOrderCurrencyCode()): ?> <tr> <th><?= $block->escapeHtml(__('%1 / %2 rate:', $order->getOrderCurrencyCode(), $order->getBaseCurrencyCode())) ?></th> - <th><?= $block->escapeHtml($order->getBaseToOrderRate()) ?></th> + <td><?= $block->escapeHtml($order->getBaseToOrderRate()) ?></td> </tr> <?php endif; ?> </table> diff --git a/app/code/Magento/Sales/view/adminhtml/web/order/create/scripts.js b/app/code/Magento/Sales/view/adminhtml/web/order/create/scripts.js index 05449c478152f..c508a5ecdfa58 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 @@ -14,7 +14,7 @@ define([ 'prototype', 'Magento_Catalog/catalog/product/composite/configure', 'Magento_Ui/js/lib/view/utils/async' -], function(jQuery, confirm, alert, template, shippingTemplate, paymentTemplate){ +], function (jQuery, confirm, alert, template, shippingTemplate, paymentTemplate) { window.AdminOrder = new Class.create(); @@ -42,7 +42,6 @@ define([ this.isOnlyVirtualProduct = false; this.excludedPaymentMethods = []; this.summarizePrice = true; - this.timerId = null; this.shippingTemplate = template(shippingTemplate, { data: { title: jQuery.mage.__('Shipping Method'), @@ -193,35 +192,42 @@ define([ } }, - isShippingField : function(fieldId){ - if(this.shippingAsBilling){ + /** + * Checks if the field belongs to the shipping address. + * + * @param {String} fieldId + * @return {Boolean} + */ + isShippingField: function (fieldId) { + if (this.shippingAsBilling) { return fieldId.include('billing'); } + return fieldId.include('shipping'); }, - isBillingField : function(fieldId){ + /** + * Checks if the field belongs to the billing address. + * + * @param {String} fieldId + * @return {Boolean} + */ + isBillingField: function (fieldId) { return fieldId.include('billing'); }, - bindAddressFields : function(container) { - var fields = $(container).select('input', 'select', 'textarea'); - for(var i=0;i<fields.length;i++){ - Event.observe(fields[i], 'change', this.triggerChangeEvent.bind(this)); - } - }, - /** - * Calls changing address field handler after timeout to prevent multiple simultaneous calls. + * Binds events on container form fields. * - * @param {Event} event + * @param {String} container */ - triggerChangeEvent: function (event) { - if (this.timerId) { - window.clearTimeout(this.timerId); - } + bindAddressFields: function (container) { + var fields = $(container).select('input', 'select', 'textarea'), + i; - this.timerId = window.setTimeout(this.changeAddressField.bind(this), 500, event); + for (i = 0; i < fields.length; i++) { + jQuery(fields[i]).change(this.changeAddressField.bind(this)); + } }, /** @@ -235,7 +241,8 @@ define([ matchRes = field.name.match(re), type, name, - data; + data, + resetShipping = false; if (!matchRes) { return; @@ -251,12 +258,21 @@ define([ } data = data.toObject(); - if (type === 'billing' && this.shippingAsBilling || type === 'shipping' && !this.shippingAsBilling) { + if (type === 'billing' && this.shippingAsBilling) { + this.syncAddressField(this.shippingAddressContainer, field.name, field.value); + resetShipping = true; + } + + if (type === 'shipping' && !this.shippingAsBilling) { + resetShipping = true; + } + + if (resetShipping) { data['reset_shipping'] = true; } data['order[' + type + '_address][customer_address_id]'] = null; - data['shipping_as_billing'] = +this.isShippingAsBilling(); + data['shipping_as_billing'] = +this.shippingAsBilling; if (name === 'customer_address_id') { data['order[' + type + '_address][customer_address_id]'] = @@ -264,8 +280,9 @@ define([ } this.resetPaymentMethod(); + if (data['reset_shipping']) { - this.resetShippingMethod(data); + this.resetShippingMethod(); } else { this.saveData(data); @@ -275,7 +292,28 @@ define([ } }, - fillAddressFields : function(container, data){ + /** + * Set address container form field value. + * + * @param {String} container - container ID + * @param {String} fieldName - form field name + * @param {*} fieldValue - form field value + */ + syncAddressField: function (container, fieldName, fieldValue) { + var syncName; + + if (this.isBillingField(fieldName)) { + syncName = fieldName.replace('billing', 'shipping'); + } + + $(container).select('[name="' + syncName + '"]').each(function (element) { + if (~['input', 'textarea', 'select'].indexOf(element.tagName.toLowerCase())) { + element.value = fieldValue; + } + }); + }, + + fillAddressFields: function(container, data){ var regionIdElem = false; var regionIdElemValue = false; @@ -316,10 +354,15 @@ define([ fields[i].setValue(data[name] ? data[name] : ''); } - if (fields[i].changeUpdater) fields[i].changeUpdater(); + if (fields[i].changeUpdater) { + fields[i].changeUpdater(); + } + if (name == 'region' && data['region_id'] && !data['region']){ fields[i].value = data['region_id']; } + + jQuery(fields[i]).trigger('change'); } }, @@ -350,16 +393,17 @@ define([ } }, - setShippingAsBilling : function(flag){ - var data; - var areasToLoad = ['billing_method', 'shipping_address', 'totals', 'giftmessage']; + /** + * Equals shipping and billing addresses. + * + * @param {Boolean} flag + */ + setShippingAsBilling: function (flag) { + var data, + areasToLoad = ['billing_method', 'shipping_address', 'shipping_method', 'totals', 'giftmessage']; + this.disableShippingAddress(flag); - if(flag){ - data = this.serializeData(this.billingAddressContainer); - } else { - data = this.serializeData(this.shippingAddressContainer); - } - areasToLoad.push('shipping_method'); + data = this.serializeData(flag ? this.billingAddressContainer : this.shippingAddressContainer); data = data.toObject(); data['shipping_as_billing'] = flag ? 1 : 0; data['reset_shipping'] = 1; @@ -367,21 +411,18 @@ define([ }, /** - * Checks if shipping address is corresponds to billing address. - * - * @return {Boolean} + * Replace shipping method area. */ - isShippingAsBilling: function () { - return jQuery('[name="shipping_same_as_billing"]').is(':checked'); - }, - - resetShippingMethod: function() { + resetShippingMethod: function () { if (!this.isOnlyVirtualProduct) { $(this.getAreaId('shipping_method')).update(this.shippingTemplate); } }, - resetPaymentMethod: function() { + /** + * Replace payment method area. + */ + resetPaymentMethod: function () { $(this.getAreaId('billing_method')).update(this.paymentTemplate); }, @@ -391,7 +432,7 @@ define([ * @return {Boolean} */ loadShippingRates: function () { - var addressContainer = this.isShippingAsBilling() ? + var addressContainer = this.shippingAsBilling ? 'billingAddressContainer' : 'shippingAddressContainer', data = this.serializeData(this[addressContainer]).toObject(); @@ -1505,6 +1546,4 @@ define([ return this._label; } }; - }); - diff --git a/app/code/Magento/Sales/view/frontend/templates/email/shipment/track.phtml b/app/code/Magento/Sales/view/frontend/templates/email/shipment/track.phtml index 9f7146ab084df..f1cd5f2b99865 100644 --- a/app/code/Magento/Sales/view/frontend/templates/email/shipment/track.phtml +++ b/app/code/Magento/Sales/view/frontend/templates/email/shipment/track.phtml @@ -9,22 +9,23 @@ ?> <?php $_shipment = $block->getShipment() ?> <?php $_order = $block->getOrder() ?> -<?php if ($_shipment && $_order && $_shipment->getAllTracks()): ?> -<br /> -<table class="shipment-track"> - <thead> +<?php $trackCollection = $_order->getTracksCollection($_shipment->getId()) ?> +<?php if ($_shipment && $_order && $trackCollection): ?> + <br /> + <table class="shipment-track"> + <thead> <tr> <th><?= /* @escapeNotVerified */ __('Shipped By') ?></th> <th><?= /* @escapeNotVerified */ __('Tracking Number') ?></th> </tr> - </thead> - <tbody> - <?php foreach ($_shipment->getAllTracks() as $_item): ?> - <tr> - <td><?= $block->escapeHtml($_item->getTitle()) ?>:</td> - <td><?= $block->escapeHtml($_item->getNumber()) ?></td> - </tr> - <?php endforeach ?> - </tbody> -</table> + </thead> + <tbody> + <?php foreach ($trackCollection as $_item): ?> + <tr> + <td><?= $block->escapeHtml($_item->getTitle()) ?>:</td> + <td><?= $block->escapeHtml($_item->getNumber()) ?></td> + </tr> + <?php endforeach ?> + </tbody> + </table> <?php endif; ?> diff --git a/app/code/Magento/Sales/view/frontend/templates/guest/form.phtml b/app/code/Magento/Sales/view/frontend/templates/guest/form.phtml index 3ebca4d08b349..89be190588677 100644 --- a/app/code/Magento/Sales/view/frontend/templates/guest/form.phtml +++ b/app/code/Magento/Sales/view/frontend/templates/guest/form.phtml @@ -10,7 +10,7 @@ <form class="form form-orders-search" id="oar-widget-orders-and-returns-form" data-mage-init='{"ordersReturns":{}, "validation":{}}' action="<?= /* @escapeNotVerified */ $block->getActionUrl() ?>" method="post" name="guest_post"> <fieldset class="fieldset"> - <legend class="admin__legend"><span><?= /* @escapeNotVerified */ __('Order Information') ?></span></legend> + <legend class="legend"><span><?= /* @escapeNotVerified */ __('Order Information') ?></span></legend> <br> <div class="field id required"> diff --git a/app/code/Magento/SalesRule/Model/ResourceModel/Coupon/Usage.php b/app/code/Magento/SalesRule/Model/ResourceModel/Coupon/Usage.php index d8fe830718ce1..8680a91a9acc4 100644 --- a/app/code/Magento/SalesRule/Model/ResourceModel/Coupon/Usage.php +++ b/app/code/Magento/SalesRule/Model/ResourceModel/Coupon/Usage.php @@ -74,11 +74,11 @@ public function loadByCustomerCoupon(\Magento\Framework\DataObject $object, $cus $select = $connection->select()->from( $this->getMainTable() )->where( - 'customer_id =:customet_id' + 'customer_id =:customer_id' )->where( 'coupon_id = :coupon_id' ); - $data = $connection->fetchRow($select, [':coupon_id' => $couponId, ':customet_id' => $customerId]); + $data = $connection->fetchRow($select, [':coupon_id' => $couponId, ':customer_id' => $customerId]); if ($data) { $object->setData($data); } diff --git a/app/code/Magento/SalesRule/Model/Rule/Condition/Product/Subselect.php b/app/code/Magento/SalesRule/Model/Rule/Condition/Product/Subselect.php index 1e8fbf43ec3bc..b3a44fcc56011 100644 --- a/app/code/Magento/SalesRule/Model/Rule/Condition/Product/Subselect.php +++ b/app/code/Magento/SalesRule/Model/Rule/Condition/Product/Subselect.php @@ -5,6 +5,9 @@ */ namespace Magento\SalesRule\Model\Rule\Condition\Product; +/** + * Subselect conditions for product. + */ class Subselect extends \Magento\SalesRule\Model\Rule\Condition\Product\Combine { /** @@ -161,9 +164,12 @@ public function validate(\Magento\Framework\Model\AbstractModel $model) } } if ($hasValidChild || parent::validate($item)) { - $total += (($hasValidChild && $useChildrenTotal) ? $childrenAttrTotal : $item->getData($attr)); + $total += ($hasValidChild && $useChildrenTotal) + ? $childrenAttrTotal * $item->getQty() + : $item->getData($attr); } } + return $this->validateAttribute($total); } } diff --git a/app/code/Magento/SalesRule/Observer/AssignCouponDataAfterOrderCustomerAssignObserver.php b/app/code/Magento/SalesRule/Observer/AssignCouponDataAfterOrderCustomerAssignObserver.php new file mode 100644 index 0000000000000..d9699d334ff6a --- /dev/null +++ b/app/code/Magento/SalesRule/Observer/AssignCouponDataAfterOrderCustomerAssignObserver.php @@ -0,0 +1,52 @@ +<?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; +use Magento\SalesRule\Model\Coupon\UpdateCouponUsages; +use Magento\Sales\Api\Data\OrderInterface; +use Magento\Framework\Event\ObserverInterface; + +class AssignCouponDataAfterOrderCustomerAssignObserver implements ObserverInterface +{ + const EVENT_KEY_CUSTOMER = 'customer'; + + const EVENT_KEY_ORDER = 'order'; + + /** + * @var UpdateCouponUsages + */ + private $updateCouponUsages; + + /** + * AssignCouponDataAfterOrderCustomerAssign constructor. + * + * @param UpdateCouponUsages $updateCouponUsages + */ + public function __construct( + UpdateCouponUsages $updateCouponUsages + ) { + $this->updateCouponUsages = $updateCouponUsages; + } + + /** + * @inheritDoc + */ + 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); + } + } +} diff --git a/app/code/Magento/SalesRule/Test/Mftf/ActionGroup/AdminCartPriceRuleActionGroup.xml b/app/code/Magento/SalesRule/Test/Mftf/ActionGroup/AdminCartPriceRuleActionGroup.xml index b9efe4e51a51c..9152016e709e5 100644 --- a/app/code/Magento/SalesRule/Test/Mftf/ActionGroup/AdminCartPriceRuleActionGroup.xml +++ b/app/code/Magento/SalesRule/Test/Mftf/ActionGroup/AdminCartPriceRuleActionGroup.xml @@ -6,13 +6,13 @@ */ --> <actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" - xsi:noNamespaceSchemaLocation="../../../../../../../dev/tests/acceptance/vendor/magento/magento2-functional-testing-framework/src/Magento/FunctionalTestingFramework/Test/etc/actionGroupSchema.xsd"> + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> <actionGroup name="DeleteCartPriceRuleByName"> <arguments> <argument name="ruleName" type="string"/> </arguments> <amOnPage url="{{AdminCartPriceRulesPage.url}}" stepKey="amOnCartPriceList"/> - <waitForPageLoad stepKey="waitForPriceList"/> + <conditionalClick selector="{{AdminDataGridHeaderSection.clearFilters}}" dependentSelector="{{AdminDataGridHeaderSection.clearFilters}}" visible="true" stepKey="clearFilters"/> <fillField selector="{{AdminCartPriceRulesSection.filterByNameInput}}" userInput="{{ruleName}}" stepKey="filterByName"/> <click selector="{{AdminCartPriceRulesSection.searchButton}}" stepKey="doFilter"/> <click selector="{{AdminCartPriceRulesSection.rowByIndex('1')}}" stepKey="goToEditRulePage"/> diff --git a/app/code/Magento/SalesRule/Test/Mftf/ActionGroup/AdminCreateCartPriceRuleActionGroup.xml b/app/code/Magento/SalesRule/Test/Mftf/ActionGroup/AdminCreateCartPriceRuleActionGroup.xml index 26a71e4167ffa..c5f35aef6f480 100644 --- a/app/code/Magento/SalesRule/Test/Mftf/ActionGroup/AdminCreateCartPriceRuleActionGroup.xml +++ b/app/code/Magento/SalesRule/Test/Mftf/ActionGroup/AdminCreateCartPriceRuleActionGroup.xml @@ -23,4 +23,31 @@ <click selector="{{AdminCartPriceRulesFormSection.save}}" stepKey="clickSaveButton"/> <see selector="{{AdminMessagesSection.success}}" userInput="You saved the rule." stepKey="seeSuccessMessage"/> </actionGroup> + + <actionGroup name="AdminCreateCartPriceRuleWithProductSubselectionCondition" extends="AdminCreateCartPriceRuleActionGroup"> + <arguments> + <argument name="condition" type="string" defaultValue="Category" /> + <argument name="operation" type="string" defaultValue="equals or greater than" /> + <argument name="totalQuantity" type="string" defaultValue="2" /> + <argument name="value" type="string" defaultValue="_defaultCategory.name" /> + </arguments> + <!--Go to Conditions section--> + <click selector="{{AdminCartPriceRulesFormSection.conditionsHeader}}" after="selectActionType" stepKey="openConditionsSection"/> + <click selector="{{AdminCartPriceRulesFormSection.addCondition('1')}}" after="openConditionsSection" stepKey="addFirstCondition"/> + <selectOption selector="{{AdminCartPriceRulesFormSection.ruleCondition('1')}}" userInput="Products subselection" after="addFirstCondition" stepKey="selectRule"/> + <waitForElementVisible selector="{{AdminCartPriceRulesFormSection.ruleParameter('is')}}" after="selectRule" stepKey="waitForFirstRuleElement"/> + <click selector="{{AdminCartPriceRulesFormSection.ruleParameter('is')}}" after="waitForFirstRuleElement" stepKey="clickToChangeRule"/> + <selectOption selector="{{AdminCartPriceRulesFormSection.ruleOperatorSelect('1--1')}}" userInput="{{operation}}" after="clickToChangeRule" stepKey="selectRule1"/> + <waitForElementVisible selector="{{AdminCartPriceRulesFormSection.ruleParameter('...')}}" after="selectRule1" stepKey="waitForSecondRuleElement"/> + <click selector="{{AdminCartPriceRulesFormSection.ruleParameter('...')}}" after="waitForSecondRuleElement" stepKey="clickToChangeRule1"/> + <fillField selector="{{AdminCartPriceRulesFormSection.ruleValueInput('1--1')}}" userInput="{{totalQuantity}}" after="clickToChangeRule1" stepKey="fillRule"/> + <click selector="{{AdminCartPriceRulesFormSection.addCondition('1--1')}}" after="fillRule" stepKey="addSecondCondition"/> + <selectOption selector="{{AdminCartPriceRulesFormSection.ruleCondition('1--1')}}" userInput="{{condition}}" after="addSecondCondition" stepKey="selectSecondCondition"/> + <waitForElementVisible selector="{{AdminCartPriceRulesFormSection.ruleParameter('...')}}" after="selectSecondCondition" stepKey="waitForThirdRuleElement"/> + <click selector="{{AdminCartPriceRulesFormSection.ruleParameter('...')}}" after="waitForThirdRuleElement" stepKey="addThirdCondition"/> + <waitForElementVisible selector="{{AdminCartPriceRulesFormSection.chooseValue('1--1--1')}}" after="addThirdCondition" stepKey="waitForForthRuleElement"/> + <click selector="{{AdminCartPriceRulesFormSection.chooseValue('1--1--1')}}" after="waitForForthRuleElement" stepKey="chooseValue"/> + <waitForElementVisible selector="{{AdminCartPriceRulesFormSection.categoryCheckbox(value)}}" after="chooseValue" stepKey="waitForCategoryVisible"/> + <checkOption selector="{{AdminCartPriceRulesFormSection.categoryCheckbox(value)}}" after="waitForCategoryVisible" stepKey="checkCategoryName"/> + </actionGroup> </actionGroups> diff --git a/app/code/Magento/SalesRule/Test/Mftf/ActionGroup/AdminEditCartPriceRuleActionGroup.xml b/app/code/Magento/SalesRule/Test/Mftf/ActionGroup/AdminEditCartPriceRuleActionGroup.xml new file mode 100644 index 0000000000000..0f18819e3ed1e --- /dev/null +++ b/app/code/Magento/SalesRule/Test/Mftf/ActionGroup/AdminEditCartPriceRuleActionGroup.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="ChangeCartPriceRuleWebsiteActionGroup"> + <arguments> + <argument name="websiteLabel" type="string"/> + </arguments> + <waitForPageLoad stepKey="waitForPriceList"/> + <selectOption selector="{{AdminCartPriceRulesFormSection.websites}}" userInput="{{websiteLabel}}" stepKey="selectWebsites"/> + <click selector="{{AdminCartPriceRulesFormSection.save}}" stepKey="clickSaveButton"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/SalesRule/Test/Mftf/ActionGroup/AdminFilterCartPriceRuleActionGroup.xml b/app/code/Magento/SalesRule/Test/Mftf/ActionGroup/AdminFilterCartPriceRuleActionGroup.xml new file mode 100644 index 0000000000000..2c44fdf3e900f --- /dev/null +++ b/app/code/Magento/SalesRule/Test/Mftf/ActionGroup/AdminFilterCartPriceRuleActionGroup.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"> + <!--Search grid with keyword search--> + <actionGroup name="AdminFilterCartPriceRuleActionGroup"> + <arguments> + <argument name="ruleName"/> + </arguments> + <conditionalClick selector="{{AdminDataGridHeaderSection.clearFilters}}" dependentSelector="{{AdminDataGridHeaderSection.clearFilters}}" visible="true" stepKey="clickClearFilters"/> + <fillField selector="{{AdminCartPriceRulesSection.filterByNameInput}}" userInput="{{ruleName}}" stepKey="filterByName"/> + <click selector="{{AdminCartPriceRulesSection.searchButton}}" stepKey="doFilter"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/SalesRule/Test/Mftf/ActionGroup/ApplyCartRuleOnStorefrontActionGroup.xml b/app/code/Magento/SalesRule/Test/Mftf/ActionGroup/ApplyCartRuleOnStorefrontActionGroup.xml index d3930a2767b0d..c800053fb1d2b 100644 --- a/app/code/Magento/SalesRule/Test/Mftf/ActionGroup/ApplyCartRuleOnStorefrontActionGroup.xml +++ b/app/code/Magento/SalesRule/Test/Mftf/ActionGroup/ApplyCartRuleOnStorefrontActionGroup.xml @@ -22,4 +22,26 @@ <click selector="{{AdminCartPriceRuleDiscountSection.applyCodeBtn}}" stepKey="applyCode"/> <waitForPageLoad stepKey="waitForApplyCode"/> </actionGroup> + + <!-- Apply Sales Rule Coupon to the cart --> + <actionGroup name="StorefrontApplyCouponActionGroup"> + <arguments> + <argument name="couponCode" type="string"/> + </arguments> + <waitForElement selector="{{AdminCartPriceRuleDiscountSection.discountTab}}" time="30" stepKey="waitForCouponHeader" /> + <conditionalClick selector="{{AdminCartPriceRuleDiscountSection.discountTab}}" dependentSelector="{{AdminCartPriceRuleDiscountSection.discountBlockActive}}" visible="false" stepKey="clickCouponHeader" /> + <waitForElementVisible selector="{{AdminCartPriceRuleDiscountSection.couponInput}}" stepKey="waitForCouponField" /> + <fillField userInput="{{couponCode}}" selector="{{AdminCartPriceRuleDiscountSection.couponInput}}" stepKey="fillCouponField"/> + <click selector="{{AdminCartPriceRuleDiscountSection.applyCodeBtn}}" stepKey="clickApplyButton"/> + <see userInput='You used coupon code "{{couponCode}}".' selector="{{StorefrontMessagesSection.successMessage}}" stepKey="seeSuccessMessage"/> + </actionGroup> + + <!-- Cancel Sales Rule Coupon applied to the cart --> + <actionGroup name="StorefrontCancelCouponActionGroup"> + <waitForElement selector="{{AdminCartPriceRuleDiscountSection.discountTab}}" time="30" stepKey="waitForCouponHeader" /> + <conditionalClick selector="{{AdminCartPriceRuleDiscountSection.discountTab}}" dependentSelector="{{AdminCartPriceRuleDiscountSection.discountBlockActive}}" visible="false" stepKey="clickCouponHeader" /> + <waitForElementVisible selector="{{AdminCartPriceRuleDiscountSection.couponInput}}" stepKey="waitForCouponField" /> + <click selector="{{AdminCartPriceRuleDiscountSection.cancelButton}}" stepKey="clickCancelButton"/> + <see userInput="You canceled the coupon code." selector="{{StorefrontMessagesSection.successMessage}}" stepKey="seeSuccessMessage"/> + </actionGroup> </actionGroups> diff --git a/app/code/Magento/SalesRule/Test/Mftf/ActionGroup/StorefrontSalesRuleActionGroup.xml b/app/code/Magento/SalesRule/Test/Mftf/ActionGroup/StorefrontSalesRuleActionGroup.xml new file mode 100644 index 0000000000000..35feabc8d9fbe --- /dev/null +++ b/app/code/Magento/SalesRule/Test/Mftf/ActionGroup/StorefrontSalesRuleActionGroup.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="VerifyDiscountAmount"> + <arguments> + <argument name="expectedDiscount" type="string"/> + </arguments> + <amOnPage url="{{CheckoutCartPage.url}}" stepKey="goToCartPage"/> + <waitForElementVisible selector="{{StorefrontCheckoutCartSummarySection.discountAmount}}" stepKey="waitForDiscountElement"/> + <see selector="{{StorefrontCheckoutCartSummarySection.discountAmount}}" userInput="{{expectedDiscount}}" stepKey="seeDiscountTotal"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/SalesRule/Test/Mftf/Data/CouponData.xml b/app/code/Magento/SalesRule/Test/Mftf/Data/CouponData.xml index d06903d5fd5d9..8810157092d97 100644 --- a/app/code/Magento/SalesRule/Test/Mftf/Data/CouponData.xml +++ b/app/code/Magento/SalesRule/Test/Mftf/Data/CouponData.xml @@ -15,4 +15,11 @@ <data key="times_used">0</data> <data key="is_primary">false</data> </entity> + + <entity name="SimpleSalesRuleCoupon" type="coupon"> + <var key="rule_id" entityKey="rule_id" entityType="SalesRule"/> + <data key="code" unique="suffix">couponCode</data> + <data key="is_primary">1</data> + <data key="times_used">0</data> + </entity> </entities> diff --git a/app/code/Magento/SalesRule/Test/Mftf/Data/SalesRuleData.xml b/app/code/Magento/SalesRule/Test/Mftf/Data/SalesRuleData.xml index 28d61e34339ef..595aedc2b604f 100644 --- a/app/code/Magento/SalesRule/Test/Mftf/Data/SalesRuleData.xml +++ b/app/code/Magento/SalesRule/Test/Mftf/Data/SalesRuleData.xml @@ -7,7 +7,7 @@ --> <entities xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" - xsi:noNamespaceSchemaLocation="../../../../../../../dev/tests/acceptance/vendor/magento/magento2-functional-testing-framework/src/Magento/FunctionalTestingFramework/DataGenerator/etc/dataProfileSchema.xsd"> + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/dataProfileSchema.xsd"> <entity name="ApiSalesRule" type="SalesRule"> <data key="name" unique="suffix">salesRule</data> <data key="description">Sales Rule Descritpion</data> @@ -56,7 +56,7 @@ </entity> <entity name="SalesRuleSpecificCoupon" type="SalesRule"> <data key="name" unique="suffix">SimpleSalesRule</data> - <data key="description">Sales Rule Descritpion</data> + <data key="description">Sales Rule Description</data> <array key="website_ids"> <item>1</item> </array> @@ -85,4 +85,95 @@ <entity name="SalesRule100PercentDiscount" extends="TestSalesRule" type="SalesRule"> <data key="discountAmount">100</data> </entity> + <entity name="SaleRule50PercentDiscountNoCoupon" type="SalesRule"> + <data key="name" unique="suffix">salesRule</data> + <data key="description">Sales Rule Descritpion</data> + <array key="website_ids"> + <item>1</item> + </array> + <array key="customer_group_ids"> + <item>0</item> + <item>1</item> + <item>3</item> + </array> + <data key="uses_per_customer">0</data> + <data key="is_active">true</data> + <data key="stop_rules_processing">true</data> + <data key="is_advanced">true</data> + <data key="sort_order">0</data> + <data key="simple_action">by_percent</data> + <data key="discount_amount">50</data> + <data key="discount_qty">0</data> + <data key="discount_step">0</data> + <data key="apply_to_shipping">false</data> + <data key="times_used">0</data> + <data key="is_rss">true</data> + <data key="coupon_type">NO_COUPON</data> + <data key="use_auto_generation">false</data> + <data key="uses_per_coupon">0</data> + <data key="simple_free_shipping">0</data> + </entity> + <entity name="ApiCartRule" type="SalesRule"> + <data key="name" unique="suffix">salesRule</data> + <data key="description">Sales Rule Descritpion</data> + <array key="website_ids"> + <item>1</item> + </array> + <array key="customer_group_ids"> + <item>0</item> + <item>1</item> + <item>3</item> + </array> + <data key="uses_per_customer">0</data> + <data key="is_active">true</data> + <data key="stop_rules_processing">true</data> + <data key="is_advanced">true</data> + <data key="sort_order">0</data> + <data key="simple_action">by_percent</data> + <data key="discount_amount">50</data> + <data key="discount_qty">0</data> + <data key="discount_step">0</data> + <data key="apply_to_shipping">false</data> + <data key="times_used">0</data> + <data key="is_rss">true</data> + <data key="coupon_type">NO_COUPON</data> + <data key="use_auto_generation">false</data> + <data key="uses_per_coupon">0</data> + <data key="simple_free_shipping">0</data> + </entity> + + <entity name="SalesRuleSpecificCouponWithFixedDiscount" type="SalesRule"> + <data key="name" unique="suffix">SimpleSalesRule</data> + <data key="description">Sales Rule Description</data> + <array key="website_ids"> + <item>1</item> + </array> + <array key="customer_group_ids"> + <item>1</item> + </array> + <data key="uses_per_customer">10</data> + <data key="is_active">true</data> + <data key="stop_rules_processing">false</data> + <data key="is_advanced">true</data> + <data key="sort_order">1</data> + <data key="simple_action">cart_fixed</data> + <data key="discount_amount">10</data> + <data key="discount_qty">10</data> + <data key="discount_step">0</data> + <data key="apply_to_shipping">false</data> + <data key="times_used">0</data> + <data key="is_rss">false</data> + <data key="coupon_type">SPECIFIC_COUPON</data> + <data key="use_auto_generation">false</data> + <data key="uses_per_coupon">10</data> + <data key="simple_free_shipping">1</data> + </entity> + + <entity name="PriceRuleWithCondition" type="SalesRule"> + <data key="name" unique="suffix">SalesRule</data> + <data key="websites">Main Website</data> + <data key="customerGroups">'NOT LOGGED IN', 'General', 'Wholesale', 'Retailer'</data> + <data key="apply">Fixed amount discount for whole cart</data> + <data key="discountAmount">0</data> + </entity> </entities> diff --git a/app/code/Magento/SalesRule/Test/Mftf/Data/SalesRuleProductConditionsData.xml b/app/code/Magento/SalesRule/Test/Mftf/Data/SalesRuleProductConditionsData.xml new file mode 100644 index 0000000000000..8af7ac0fdd99a --- /dev/null +++ b/app/code/Magento/SalesRule/Test/Mftf/Data/SalesRuleProductConditionsData.xml @@ -0,0 +1,16 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<entities xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:DataGenerator/etc/dataProfileSchema.xsd"> + <entity name="SalesRuleProductConditions" type="SalesRuleConditionAttribute"> + <data key="priceInCart" >Magento\SalesRule\Model\Rule\Condition\Product|quote_item_price</data> + <data key="quantityInCart">Magento\SalesRule\Model\Rule\Condition\Product|quote_item_qty</data> + <data key="rowTotalInCart">Magento\SalesRule\Model\Rule\Condition\Product|quote_item_row_total</data> + </entity> +</entities> diff --git a/app/code/Magento/SalesRule/Test/Mftf/Metadata/coupon-meta.xml b/app/code/Magento/SalesRule/Test/Mftf/Metadata/coupon-meta.xml index dda402ed6436d..9bec2ecd006cf 100644 --- a/app/code/Magento/SalesRule/Test/Mftf/Metadata/coupon-meta.xml +++ b/app/code/Magento/SalesRule/Test/Mftf/Metadata/coupon-meta.xml @@ -9,7 +9,7 @@ <operations xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="../../../../../../../dev/tests/acceptance/vendor/magento/magento2-functional-testing-framework/src/Magento/FunctionalTestingFramework/DataGenerator/etc/dataOperation.xsd"> - <operation name="CreateCoupon" dataType="coupon" type="create" auth="adminOauth" url="/rest/V1/coupons" method="POST"> + <operation name="CreateCoupon" dataType="coupon" type="create" auth="adminOauth" url="/V1/coupons" method="POST"> <contentType>application/json</contentType> <object key="coupon" dataType="coupon"> <field key="rule_id" required="true">integer</field> diff --git a/app/code/Magento/SalesRule/Test/Mftf/Page/AdminCartPriceRuleEditPage.xml b/app/code/Magento/SalesRule/Test/Mftf/Page/AdminCartPriceRuleEditPage.xml new file mode 100644 index 0000000000000..faed9d42bcdec --- /dev/null +++ b/app/code/Magento/SalesRule/Test/Mftf/Page/AdminCartPriceRuleEditPage.xml @@ -0,0 +1,13 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<pages xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Page/etc/PageObject.xsd"> + <page name="AdminCartPriceRuleEditPage" area="admin" url="sales_rule/promo_quote/edit/id/{{salesRuleId}}" module="Magento_SalesRule" parameterized="true"> + <section name="AdminCartPriceRulesFormSection"/> + </page> +</pages> diff --git a/app/code/Magento/SalesRule/Test/Mftf/Page/AdminCartPriceRulesLimitPage.xml b/app/code/Magento/SalesRule/Test/Mftf/Page/AdminCartPriceRulesLimitPage.xml new file mode 100644 index 0000000000000..3cd2563ddf183 --- /dev/null +++ b/app/code/Magento/SalesRule/Test/Mftf/Page/AdminCartPriceRulesLimitPage.xml @@ -0,0 +1,10 @@ +<?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:Test/etc/PageObject.xsd"> + <page name="AdminCartPriceRulesLimitPage" url="sales_rule/promo_quote/index/limit/{{count}}" area="admin" module="Magento_SalesRule" parameterized="true"/> +</pages> diff --git a/app/code/Magento/SalesRule/Test/Mftf/Section/AdminCartPriceRuleDiscountSection.xml b/app/code/Magento/SalesRule/Test/Mftf/Section/AdminCartPriceRuleDiscountSection.xml index d0aaa8e15f9ca..257452d6cd27a 100644 --- a/app/code/Magento/SalesRule/Test/Mftf/Section/AdminCartPriceRuleDiscountSection.xml +++ b/app/code/Magento/SalesRule/Test/Mftf/Section/AdminCartPriceRuleDiscountSection.xml @@ -6,10 +6,12 @@ */ --> <sections xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" - xsi:noNamespaceSchemaLocation="urn:magento:mftf:Page/etc/SectionObject.xsd"> + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Page/etc/SectionObject.xsd"> <section name="AdminCartPriceRuleDiscountSection"> - <element name="discountTab" type="button" selector="//strong[text()='Apply Discount Code']"/> + <element name="discountTab" type="button" selector="#block-discount-heading"/> <element name="couponInput" type="input" selector="#coupon_code"/> - <element name="applyCodeBtn" type="button" selector="//span[text()='Apply Discount']"/> + <element name="applyCodeBtn" type="button" selector="#discount-coupon-form button[class*='apply']" timeout="30"/> + <element name="discountBlockActive" type="text" selector=".block.discount.active"/> + <element name="cancelButton" type="text" selector="#discount-coupon-form button[class*='cancel']" timeout="30"/> </section> </sections> diff --git a/app/code/Magento/SalesRule/Test/Mftf/Section/AdminCartPriceRulesFormSection.xml b/app/code/Magento/SalesRule/Test/Mftf/Section/AdminCartPriceRulesFormSection.xml index 14584acd03376..8c364879ab1f4 100644 --- a/app/code/Magento/SalesRule/Test/Mftf/Section/AdminCartPriceRulesFormSection.xml +++ b/app/code/Magento/SalesRule/Test/Mftf/Section/AdminCartPriceRulesFormSection.xml @@ -24,8 +24,19 @@ <element name="userPerCustomer" type="input" selector="//input[@name='uses_per_customer']"/> <element name="priority" type="input" selector="//*[@name='sort_order']"/> + <!-- Conditions sub-form --> + <element name="conditionsHeader" type="button" selector="div[data-index='conditions']" timeout="30"/> + <element name="addCondition" type="button" selector="//*[@id='conditions__{{arg}}__children']//span" parameterized="true"/> + <element name="ruleCondition" type="select" selector="rule[conditions][{{arg}}][new_child]" parameterized="true"/> + <element name="ruleParameter" type="text" selector="//span[@class='rule-param']/a[contains(text(), '{{arg}}')]" parameterized="true"/> + <element name="ruleOperatorSelect" type="select" selector="rule[conditions][{{arg}}][operator]" parameterized="true"/> + <element name="ruleValueInput" type="input" selector="rule[conditions][{{arg}}][value]" parameterized="true"/> + <element name="chooseValue" type="button" selector="label[for='conditions__{{arg}}__value']" parameterized="true"/> + <element name="categoryCheckbox" type="checkbox" selector="//span[contains(text(), '{{arg}}')]/parent::a/preceding-sibling::input[@type='checkbox']" parameterized="true"/> + <!-- Actions sub-form --> <element name="actionsHeader" type="button" selector="div[data-index='actions']" timeout="30"/> + <element name="actionsHeaderOpen" type="button" selector="div[data-index='actions'] div[data-state-collapsible='open']" timeout="30"/> <element name="apply" type="select" selector="select[name='simple_action']"/> <element name="applyDiscountToShipping" type="checkbox" selector="input[name='apply_to_shipping']"/> <element name="applyDiscountToShippingLabel" type="checkbox" selector="input[name='apply_to_shipping']+label"/> @@ -37,6 +48,8 @@ <element name="operator" type="select" selector="select[name*='[operator]']"/> <element name="childAttribute" type="select" selector="select[name*='new_child']"/> <element name="optionInput" type="input" selector="ul[class*='rule-param-children'] input[name*='[value]']"/> + <element name="actionValue" type="input" selector=".rule-param-edit input"/> + <element name="actionOperator" type="select" selector=".rule-param-edit select"/> <!-- Manage Coupon Codes sub-form --> <element name="manageCouponCodesHeader" type="button" selector="div[data-index='manage_coupon_codes']" timeout="30"/> diff --git a/app/code/Magento/SalesRule/Test/Mftf/Section/AdminCartPriceRulesSection.xml b/app/code/Magento/SalesRule/Test/Mftf/Section/AdminCartPriceRulesSection.xml index a341c4a3bf769..0aa01e06c44c7 100644 --- a/app/code/Magento/SalesRule/Test/Mftf/Section/AdminCartPriceRulesSection.xml +++ b/app/code/Magento/SalesRule/Test/Mftf/Section/AdminCartPriceRulesSection.xml @@ -6,7 +6,7 @@ */ --> <sections xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" - xsi:noNamespaceSchemaLocation="../../../../../../../dev/tests/acceptance/vendor/magento/magento2-functional-testing-framework/src/Magento/FunctionalTestingFramework/Page/etc/SectionObject.xsd"> + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/SectionObject.xsd"> <section name="AdminCartPriceRulesSection"> <element name="addNewRuleButton" type="button" selector="#add" timeout="30"/> <element name="messages" type="text" selector=".messages"/> @@ -15,5 +15,9 @@ <element name="rowByIndex" type="text" selector="tr[data-role='row']:nth-of-type({{var1}})" parameterized="true" timeout="30"/> <element name="nameColumns" type="text" selector="td[data-column='name']"/> <element name="rowContainingText" type="text" selector="//*[@id='promo_quote_grid_table']/tbody/tr[td//text()[contains(., '{{var1}}')]]" parameterized="true" timeout="30"/> + <element name="rulesRow" type="text" selector="//tr[@data-role='row']"/> + <element name="pageCurrent" type="text" selector="//label[@for='promo_quote_grid_page-current']"/> + <element name="countPages" type="text" selector="//label[@for='promo_quote_grid_page-current']//span"/> + <element name="totalCount" type="text" selector="span[data-ui-id*='grid-total-count']"/> </section> </sections> diff --git a/app/code/Magento/SalesRule/Test/Mftf/Test/AdminCartRulesAppliedForProductInCartTest.xml b/app/code/Magento/SalesRule/Test/Mftf/Test/AdminCartRulesAppliedForProductInCartTest.xml new file mode 100644 index 0000000000000..7da6ca3fbe072 --- /dev/null +++ b/app/code/Magento/SalesRule/Test/Mftf/Test/AdminCartRulesAppliedForProductInCartTest.xml @@ -0,0 +1,107 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> + <test name="AdminCartRulesAppliedForProductInCartTest"> + <annotations> + <features value="SalesRule"/> + <stories value="Create cart price rule"/> + <title value="Check that cart rules applied for product in cart"/> + <description value="Check that cart rules applied for product in cart"/> + <severity value="MAJOR"/> + <testCaseId value="MC-13629"/> + <useCaseId value="MAGETWO-94348"/> + <group value="salesRule"/> + </annotations> + + <before> + <actionGroup ref="LoginAsAdmin" stepKey="loginAsAdmin"/> + + <!--Create category and product--> + <createData entity="_defaultCategory" stepKey="createPreReqCategory"/> + <createData entity="SimpleProduct2" stepKey="simpleProduct"> + <field key="price">200</field> + <field key="quantity">500</field> + <requiredEntity createDataKey="createPreReqCategory"/> + </createData> + </before> + <after> + <!--Delete created data--> + <deleteData createDataKey="createPreReqCategory" stepKey="deleteCategory"/> + <deleteData createDataKey="simpleProduct" stepKey="deleteSimpleProduct"/> + + <actionGroup ref="DeleteProductOnProductsGridPageByName" stepKey="deleteProductOnProductsGridPageByName"> + <argument name="product" value="BundleProduct"/> + </actionGroup> + <actionGroup ref="clearFiltersAdminDataGrid" stepKey="clearFilters"/> + + <actionGroup ref="DeleteCartPriceRuleByName" stepKey="deleteCartPriceRule"> + <argument name="ruleName" value="{{PriceRuleWithCondition.name}}"/> + </actionGroup> + <actionGroup ref="clearFiltersAdminDataGrid" stepKey="clearFilters1"/> + + <actionGroup ref="logout" stepKey="logout"/> + </after> + + <!--Start creating a bundle product--> + <amOnPage url="{{AdminProductIndexPage.url}}" stepKey="goToProductList"/> + <actionGroup ref="goToCreateProductPage" stepKey="goToCreateProduct"> + <argument name="product" value="BundleProduct"/> + </actionGroup> + + <!--Off dynamic price and set value--> + <click selector="{{AdminProductFormBundleSection.priceTypeSwitcher}}" stepKey="offDynamicPrice"/> + <fillField selector="{{AdminProductFormSection.productPrice}}" userInput="0" stepKey="setProductPrice"/> + + <!-- Add option, a "Radio Buttons" type option, with one product and set fixed price 200--> + <actionGroup ref="CreateBundleProductForOneSimpleProductsWithRadioTypeOption" stepKey="createBundleProductWithRadioTypeOption"> + <argument name="bundleProduct" value="BundleProduct"/> + <argument name="simpleProductFirst" value="$$simpleProduct$$"/> + <argument name="simpleProductSecond"/> + </actionGroup> + <selectOption selector="{{AdminProductFormBundleSection.bundleSelectionPriceType}}" userInput="Fixed" stepKey="selectPriceType"/> + <fillField selector="{{AdminProductFormBundleSection.bundleSelectionPriceValue}}" userInput="200" stepKey="fillPriceValue"/> + <actionGroup ref="saveProductForm" stepKey="saveProduct"/> + + <!--Create cart price rule--> + <actionGroup ref="AdminCreateCartPriceRuleWithProductSubselectionCondition" stepKey="createRule"> + <argument name="rule" value="PriceRuleWithCondition"/> + <argument name="condition" value="Category"/> + <argument name="operation" value="equals or greater than"/> + <argument name="totalQuantity" value="2"/> + <argument name="value" value="{{_defaultCategory.name}}"/> + </actionGroup> + + <!--Go to Storefront and add product to cart and checkout from cart--> + <amOnPage url="{{StorefrontProductPage.url($$simpleProduct.name$$)}}" stepKey="goToProduct"/> + <actionGroup ref="StorefrontAddProductToCartQuantityActionGroup" stepKey="addToCart"> + <argument name="productName" value="$$simpleProduct.name$$"/> + <argument name="quantity" value="2"/> + </actionGroup> + <actionGroup ref="GoToCheckoutFromMinicartActionGroup" stepKey="goToCheckoutFromMinicart"/> + <actionGroup ref="GuestCheckoutFillingShippingSectionActionGroup" stepKey="guestCheckoutFillingShipping"/> + + <!--Check totals--> + <grabTextFrom selector="{{CheckoutPaymentSection.orderSummarySubtotal}}" stepKey="grabSubtotal"/> + <grabTextFrom selector="{{CheckoutPaymentSection.orderSummaryShippingTotal}}" stepKey="grabShippingTotal"/> + <grabTextFrom selector="{{CheckoutPaymentSection.orderSummaryTotal}}" stepKey="grabTotal"/> + <assertEquals stepKey="assertSubtotal"> + <expectedResult type="string">$400.00</expectedResult> + <actualResult type="variable">$grabSubtotal</actualResult> + </assertEquals> + <assertEquals stepKey="assertShippingTotal"> + <expectedResult type="string">$10.00</expectedResult> + <actualResult type="variable">$grabShippingTotal</actualResult> + </assertEquals> + <assertEquals stepKey="assertTotal"> + <expectedResult type="string">$410.00</expectedResult> + <actualResult type="variable">$grabTotal</actualResult> + </assertEquals> + </test> +</tests> diff --git a/app/code/Magento/SalesRule/etc/events.xml b/app/code/Magento/SalesRule/etc/events.xml index 8261860bbb7ce..eec0da74f619e 100644 --- a/app/code/Magento/SalesRule/etc/events.xml +++ b/app/code/Magento/SalesRule/etc/events.xml @@ -24,4 +24,7 @@ <event name="magento_salesrule_api_data_ruleinterface_load_after"> <observer name="legacy_model_load" instance="Magento\Framework\EntityManager\Observer\AfterEntityLoad" /> </event> + <event name="sales_order_customer_assign_after"> + <observer name="sales_order_assign_customer_after" instance="Magento\SalesRule\Observer\AssignCouponDataAfterOrderCustomerAssignObserver" /> + </event> </config> diff --git a/app/code/Magento/SalesRule/view/frontend/layout/checkout_cart_index.xml b/app/code/Magento/SalesRule/view/frontend/layout/checkout_cart_index.xml index 022403579b237..375324ed4cde6 100644 --- a/app/code/Magento/SalesRule/view/frontend/layout/checkout_cart_index.xml +++ b/app/code/Magento/SalesRule/view/frontend/layout/checkout_cart_index.xml @@ -13,14 +13,10 @@ <item name="components" xsi:type="array"> <item name="block-totals" xsi:type="array"> <item name="children" xsi:type="array"> - <item name="before_grandtotal" xsi:type="array"> - <item name="children" xsi:type="array"> - <item name="discount" xsi:type="array"> - <item name="component" xsi:type="string">Magento_SalesRule/js/view/cart/totals/discount</item> - <item name="config" xsi:type="array"> - <item name="title" xsi:type="string" translate="true">Discount</item> - </item> - </item> + <item name="discount" xsi:type="array"> + <item name="component" xsi:type="string">Magento_SalesRule/js/view/cart/totals/discount</item> + <item name="config" xsi:type="array"> + <item name="title" xsi:type="string" translate="true">Discount</item> </item> </item> </item> diff --git a/app/code/Magento/Shipping/Block/Adminhtml/Order/Packaging.php b/app/code/Magento/Shipping/Block/Adminhtml/Order/Packaging.php index b4ff445c63f4e..e5e419328eea4 100644 --- a/app/code/Magento/Shipping/Block/Adminhtml/Order/Packaging.php +++ b/app/code/Magento/Shipping/Block/Adminhtml/Order/Packaging.php @@ -74,6 +74,7 @@ public function getShipment() * Configuration for popup window for packaging * * @return string + * @SuppressWarnings(PHPMD.RequestAwareBlockMethod) */ public function getConfigDataJson() { @@ -86,7 +87,7 @@ public function getConfigDataJson() $itemsName = []; $itemsWeight = []; $itemsProductId = []; - + $itemsOrderItemId = []; if ($shipmentId) { $urlParams['shipment_id'] = $shipmentId; $createLabelUrl = $this->getUrl('adminhtml/order_shipment/createLabel', $urlParams); diff --git a/app/code/Magento/Shipping/Model/Carrier/AbstractCarrierOnline.php b/app/code/Magento/Shipping/Model/Carrier/AbstractCarrierOnline.php index 762ffed75fc34..019baeef062c5 100644 --- a/app/code/Magento/Shipping/Model/Carrier/AbstractCarrierOnline.php +++ b/app/code/Magento/Shipping/Model/Carrier/AbstractCarrierOnline.php @@ -28,6 +28,14 @@ abstract class AbstractCarrierOnline extends AbstractCarrier const GUAM_REGION_CODE = 'GU'; + const SPAIN_COUNTRY_ID = 'ES'; + + const CANARY_ISLANDS_COUNTRY_ID = 'IC'; + + const SANTA_CRUZ_DE_TENERIFE_REGION_ID = 'Santa Cruz de Tenerife'; + + const LAS_PALMAS_REGION_ID = 'Las Palmas'; + /** * Array of quotes * diff --git a/app/code/Magento/Shipping/Test/Mftf/ActionGroup/AdminShipmentActionGroup.xml b/app/code/Magento/Shipping/Test/Mftf/ActionGroup/AdminShipmentActionGroup.xml new file mode 100644 index 0000000000000..1d90867b110dd --- /dev/null +++ b/app/code/Magento/Shipping/Test/Mftf/ActionGroup/AdminShipmentActionGroup.xml @@ -0,0 +1,50 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> + <actionGroup name="VerifyBasicShipmentInformation"> + <arguments> + <argument name="customer"/> + <argument name="shippingAddress"/> + <argument name="billingAddress"/> + <argument name="customerGroup" defaultValue="GeneralCustomerGroup"/> + </arguments> + <see selector="{{AdminShipmentOrderAndAccountInformationSection.customerName}}" userInput="{{customer.firstname}}" stepKey="seeCustomerName"/> + <see selector="{{AdminShipmentOrderAndAccountInformationSection.customerEmail}}" userInput="{{customer.email}}" stepKey="seeCustomerEmail"/> + <see selector="{{AdminShipmentOrderAndAccountInformationSection.customerGroup}}" userInput="{{customerGroup.code}}" stepKey="seeCustomerGroup"/> + <see selector="{{AdminShipmentAddressInformationSection.billingAddress}}" userInput="{{billingAddress.street[0]}}" stepKey="seeBillingAddressStreet"/> + <see selector="{{AdminShipmentAddressInformationSection.billingAddress}}" userInput="{{billingAddress.city}}" stepKey="seeBillingAddressCity"/> + <see selector="{{AdminShipmentAddressInformationSection.billingAddress}}" userInput="{{billingAddress.country}}" stepKey="seeBillingAddressCountry"/> + <see selector="{{AdminShipmentAddressInformationSection.billingAddress}}" userInput="{{billingAddress.postcode}}" stepKey="seeBillingAddressPostcode"/> + <see selector="{{AdminShipmentAddressInformationSection.shippingAddress}}" userInput="{{shippingAddress.street[0]}}" stepKey="seeShippingAddressStreet"/> + <see selector="{{AdminShipmentAddressInformationSection.shippingAddress}}" userInput="{{shippingAddress.city}}" stepKey="seeShippingAddressCity"/> + <see selector="{{AdminShipmentAddressInformationSection.shippingAddress}}" userInput="{{shippingAddress.country}}" stepKey="seeShippingAddressCountry"/> + <see selector="{{AdminShipmentAddressInformationSection.shippingAddress}}" userInput="{{shippingAddress.postcode}}" stepKey="seeShippingAddressPostcode"/> + </actionGroup> + + <actionGroup name="SeeProductInShipmentItems"> + <arguments> + <argument name="product"/> + </arguments> + <seeElement selector="{{AdminShipmentItemsSection.productColumn(product.name)}}" stepKey="seeProductInShipmentItemsGrid"/> + </actionGroup> + + <actionGroup name="StartCreateShipmentFromOrderPage"> + <click selector="{{AdminOrderDetailsMainActionsSection.ship}}" stepKey="clickShipAction"/> + <seeInCurrentUrl url="{{AdminShipmentNewPage.url}}" stepKey="seeNewShipmentUrl"/> + <see selector="{{AdminHeaderSection.pageTitle}}" userInput="New Shipment" stepKey="seeNewShipmentPageTitle"/> + </actionGroup> + + <actionGroup name="SubmitShipment"> + <click selector="{{AdminShipmentMainActionsSection.submitShipment}}" stepKey="clickSubmitShipment"/> + <grabFromCurrentUrl regex="~/order_id/(\d+)/~" stepKey="grabOrderId"/> + <seeInCurrentUrl url="{{AdminOrderDetailsPage.url('$grabOrderId')}}" stepKey="seeViewOrderPageShipping"/> + <see selector="{{AdminMessagesSection.successMessage}}" userInput="The shipment has been created." stepKey="seeShipmentCreateSuccess"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/Shipping/Test/Mftf/Page/AdminShipmentNewPage.xml b/app/code/Magento/Shipping/Test/Mftf/Page/AdminShipmentNewPage.xml index d7b91f68d21a2..bd311d3390043 100644 --- a/app/code/Magento/Shipping/Test/Mftf/Page/AdminShipmentNewPage.xml +++ b/app/code/Magento/Shipping/Test/Mftf/Page/AdminShipmentNewPage.xml @@ -10,6 +10,8 @@ xsi:noNamespaceSchemaLocation="../../../../../../../dev/tests/acceptance/vendor/magento/magento2-functional-testing-framework/src/Magento/FunctionalTestingFramework/Page/etc/PageObject.xsd"> <page name="AdminShipmentNewPage" url="order_shipment/new/order_id/" area="admin" module="Magento_Shipping"> <section name="AdminShipmentMainActionsSection"/> + <section name="AdminShipmentOrderAndAccountInformationSection"/> + <section name="AdminShipmentAddressInformationSection"/> <section name="AdminShipmentItemsSection"/> </page> </pages> diff --git a/app/code/Magento/Shipping/Test/Mftf/Section/AdminShipmentAddressInformationSection.xml b/app/code/Magento/Shipping/Test/Mftf/Section/AdminShipmentAddressInformationSection.xml new file mode 100644 index 0000000000000..39035868c4b65 --- /dev/null +++ b/app/code/Magento/Shipping/Test/Mftf/Section/AdminShipmentAddressInformationSection.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="AdminShipmentAddressInformationSection"> + <element name="billingAddress" type="text" selector=".order-billing-address address"/> + <element name="billingAddressEdit" type="button" selector=".order-billing-address .actions a"/> + <element name="shippingAddress" type="text" selector=".order-shipping-address address"/> + <element name="shippingAddressEdit" type="button" selector=".order-shipping-address .actions a"/> + </section> +</sections> diff --git a/app/code/Magento/Shipping/Test/Mftf/Section/AdminShipmentItemsSection.xml b/app/code/Magento/Shipping/Test/Mftf/Section/AdminShipmentItemsSection.xml index 740cae14f8bc5..55d26d729090c 100644 --- a/app/code/Magento/Shipping/Test/Mftf/Section/AdminShipmentItemsSection.xml +++ b/app/code/Magento/Shipping/Test/Mftf/Section/AdminShipmentItemsSection.xml @@ -7,9 +7,10 @@ --> <sections xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" - xsi:noNamespaceSchemaLocation="../../../../../../../dev/tests/acceptance/vendor/magento/magento2-functional-testing-framework/src/Magento/FunctionalTestingFramework/Page/etc/SectionObject.xsd"> + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Page/etc/SectionObject.xsd"> <section name="AdminShipmentItemsSection"> <element name="itemQty" type="text" selector=".order-shipment-table tbody:nth-of-type({{var1}}) .col-ordered-qty .qty-table" parameterized="true"/> <element name="itemQtyToShip" type="input" selector=".order-shipment-table tbody:nth-of-type({{row}}) .col-qty input.qty-item" parameterized="true"/> + <element name="productColumn" type="text" selector="//*[contains(@class,'order-shipment-table')]//td[@class = 'col-product']//div[contains(text(),'{{productName}}')]" parameterized="true"/> </section> </sections> diff --git a/app/code/Magento/Shipping/Test/Mftf/Section/AdminShipmentMainActionsSection.xml b/app/code/Magento/Shipping/Test/Mftf/Section/AdminShipmentMainActionsSection.xml index d79748d1e20cc..a21c3229e3549 100644 --- a/app/code/Magento/Shipping/Test/Mftf/Section/AdminShipmentMainActionsSection.xml +++ b/app/code/Magento/Shipping/Test/Mftf/Section/AdminShipmentMainActionsSection.xml @@ -7,8 +7,8 @@ --> <sections xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" - xsi:noNamespaceSchemaLocation="../../../../../../../dev/tests/acceptance/vendor/magento/magento2-functional-testing-framework/src/Magento/FunctionalTestingFramework/Page/etc/SectionObject.xsd"> + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Page/etc/SectionObject.xsd"> <section name="AdminShipmentMainActionsSection"> - <element name="submitShipment" type="button" selector="button.action-default.save.submit-button"/> + <element name="submitShipment" type="button" selector="button.action-default.save.submit-button" timeout="30"/> </section> </sections> diff --git a/app/code/Magento/Shipping/Test/Mftf/Section/AdminShipmentOrderAndAccountInformationSection.xml b/app/code/Magento/Shipping/Test/Mftf/Section/AdminShipmentOrderAndAccountInformationSection.xml new file mode 100644 index 0000000000000..2a83995cf3bbc --- /dev/null +++ b/app/code/Magento/Shipping/Test/Mftf/Section/AdminShipmentOrderAndAccountInformationSection.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="AdminShipmentOrderAndAccountInformationSection"> + <element name="customerName" type="text" selector=".order-account-information table tr:first-of-type > td span"/> + <element name="customerEmail" type="text" selector=".order-account-information table tr:nth-of-type(2) > td a"/> + <element name="customerGroup" type="text" selector=".order-account-information table tr:nth-of-type(3) > td"/> + </section> +</sections> diff --git a/app/code/Magento/Store/Model/WebsiteRepository.php b/app/code/Magento/Store/Model/WebsiteRepository.php index 94fd59c7634df..1b12164e42cee 100644 --- a/app/code/Magento/Store/Model/WebsiteRepository.php +++ b/app/code/Magento/Store/Model/WebsiteRepository.php @@ -77,7 +77,14 @@ public function get($code) ]); if ($website->getId() === null) { - throw new NoSuchEntityException(); + throw new NoSuchEntityException( + __( + sprintf( + "The website with code %s that was requested wasn't found. Verify the website and try again.", + $code + ) + ) + ); } $this->entities[$code] = $website; $this->entitiesById[$website->getId()] = $website; @@ -99,7 +106,14 @@ public function getById($id) ]); if ($website->getId() === null) { - throw new NoSuchEntityException(); + throw new NoSuchEntityException( + __( + sprintf( + "The website with id %s that was requested wasn't found. Verify the website and try again.", + $id + ) + ) + ); } $this->entities[$website->getCode()] = $website; $this->entitiesById[$id] = $website; diff --git a/app/code/Magento/Store/Test/Mftf/ActionGroup/AdminCreateWebsiteActionGroup.xml b/app/code/Magento/Store/Test/Mftf/ActionGroup/AdminCreateWebsiteActionGroup.xml index 9361c7d942c67..9d7c538d3a3c4 100644 --- a/app/code/Magento/Store/Test/Mftf/ActionGroup/AdminCreateWebsiteActionGroup.xml +++ b/app/code/Magento/Store/Test/Mftf/ActionGroup/AdminCreateWebsiteActionGroup.xml @@ -23,4 +23,18 @@ <waitForElementVisible selector="{{AdminStoresGridSection.websiteFilterTextField}}" stepKey="waitForStoreGridToReload"/> <see userInput="You saved the website." stepKey="seeSavedMessage" /> </actionGroup> + + <!--Get Website_id--> + <actionGroup name="AdminGetWebsiteIdActionGroup"> + <arguments> + <argument name="website"/> + </arguments> + <amOnPage url="{{AdminSystemStorePage.url}}" stepKey="amOnTheStorePage"/> + <click selector="{{AdminStoresGridSection.resetButton}}" stepKey="clickOnResetButton"/> + <fillField selector="{{AdminStoresGridSection.websiteFilterTextField}}" userInput="{{website.name}}" stepKey="fillSearchWebsiteField"/> + <click selector="{{AdminStoresGridSection.searchButton}}" stepKey="clickSearchButton" /> + <see selector="{{AdminStoresGridSection.websiteNameInFirstRow}}" userInput="{{website.name}}" stepKey="verifyThatCorrectWebsiteFound"/> + <click selector="{{AdminStoresGridSection.websiteNameInFirstRow}}" stepKey="clickEditExistingWebsite"/> + <grabFromCurrentUrl regex="~(\d+)/~" stepKey="grabFromCurrentUrl"/> + </actionGroup> </actionGroups> diff --git a/app/code/Magento/Store/Test/Mftf/ActionGroup/AdminSwitchStoreViewActionGroup.xml b/app/code/Magento/Store/Test/Mftf/ActionGroup/AdminSwitchStoreViewActionGroup.xml new file mode 100644 index 0000000000000..fd86c7a0525c7 --- /dev/null +++ b/app/code/Magento/Store/Test/Mftf/ActionGroup/AdminSwitchStoreViewActionGroup.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="AdminSwitchStoreViewActionGroup"> + <arguments> + <argument name="storeView" defaultValue="customStore.name"/> + </arguments> + <click selector="{{AdminMainActionsSection.storeViewDropdown}}" stepKey="clickStoreViewSwitchDropdown"/> + <waitForElementVisible selector="{{AdminMainActionsSection.storeViewByName('Default Store View')}}" stepKey="waitForStoreViewsAreVisible"/> + <click selector="{{AdminMainActionsSection.storeViewByName(storeView)}}" stepKey="clickStoreViewByName"/> + <waitForElementVisible selector="{{AdminConfirmationModalSection.ok}}" stepKey="waitingForInformationModal"/> + <click selector="{{AdminConfirmationModalSection.ok}}" stepKey="confirmStoreSwitch"/> + <waitForPageLoad stepKey="waitForStoreViewSwitched"/> + <see userInput="{{storeView}}" selector="{{AdminMainActionsSection.storeSwitcher}}" stepKey="seeNewStoreViewName"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/Store/Test/Mftf/ActionGroup/StorefrontSwitchStoreViewActionGroup.xml b/app/code/Magento/Store/Test/Mftf/ActionGroup/StorefrontSwitchStoreViewActionGroup.xml new file mode 100644 index 0000000000000..e6ebd229e4683 --- /dev/null +++ b/app/code/Magento/Store/Test/Mftf/ActionGroup/StorefrontSwitchStoreViewActionGroup.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="StorefrontSwitchStoreViewActionGroup"> + <arguments> + <argument name="storeView" defaultValue="customStore"/> + </arguments> + <click selector="{{StorefrontHeaderSection.storeViewSwitcher}}" stepKey="clickStoreViewSwitcher"/> + <waitForElementVisible selector="{{StorefrontHeaderSection.storeViewDropdown}}" stepKey="waitForStoreViewDropdown"/> + <click selector="{{StorefrontHeaderSection.storeViewOption(storeView.name)}}" stepKey="clickSelectStoreView"/> + <waitForPageLoad stepKey="waitForPageLoad"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/Store/Test/Mftf/Data/ProductWebsiteLinkData.xml b/app/code/Magento/Store/Test/Mftf/Data/ProductWebsiteLinkData.xml new file mode 100644 index 0000000000000..8e84b84c8aa49 --- /dev/null +++ b/app/code/Magento/Store/Test/Mftf/Data/ProductWebsiteLinkData.xml @@ -0,0 +1,14 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<entities xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:DataGenerator/etc/dataProfileSchema.xsd"> + <entity name="ProductAssignToWebsite" type="product_website_link"> + <var key="sku" entityKey="sku" entityType="product"/> + </entity> +</entities> \ No newline at end of file diff --git a/app/code/Magento/Store/Test/Mftf/Data/StoreData.xml b/app/code/Magento/Store/Test/Mftf/Data/StoreData.xml index f85125bcf3291..6d4fb7e9b8154 100644 --- a/app/code/Magento/Store/Test/Mftf/Data/StoreData.xml +++ b/app/code/Magento/Store/Test/Mftf/Data/StoreData.xml @@ -5,7 +5,7 @@ * See COPYING.txt for license details. */ --> -<entities xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="../../../../../../../dev/tests/acceptance/vendor/magento/magento2-functional-testing-framework/src/Magento/FunctionalTestingFramework/DataGenerator/etc/dataProfileSchema.xsd"> +<entities xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:mftf:DataGenerator/etc/dataProfileSchema.xsd"> <entity name="_defaultStore" type="store"> <data key="name">Default Store View</data> <data key="code">default</data> @@ -39,6 +39,24 @@ <data key="store_type">store</data> <requiredEntity type="storeGroup">customStoreGroup</requiredEntity> </entity> + <entity name="CustomStoreENNotUnique" type="store"> + <data key="name">EN</data> + <data key="code">en</data> + <data key="is_active">1</data> + <data key="store_id">null</data> + <data key="store_action">add</data> + <data key="store_type">store</data> + <requiredEntity type="storeGroup">customStoreGroup</requiredEntity> + </entity> + <entity name="CustomStoreNLNotUnique" type="store"> + <data key="name">NL</data> + <data key="code">nl</data> + <data key="is_active">1</data> + <data key="store_id">null</data> + <data key="store_action">add</data> + <data key="store_type">store</data> + <requiredEntity type="storeGroup">customStoreGroup</requiredEntity> + </entity> <entity name="secondStore" type="store"> <data key="name">Second Store View</data> <data key="code">second_store_view</data> diff --git a/app/code/Magento/Store/Test/Mftf/Data/WebsiteData.xml b/app/code/Magento/Store/Test/Mftf/Data/WebsiteData.xml index 22c5d747748cd..1403175dcb7f1 100644 --- a/app/code/Magento/Store/Test/Mftf/Data/WebsiteData.xml +++ b/app/code/Magento/Store/Test/Mftf/Data/WebsiteData.xml @@ -16,6 +16,9 @@ <data key="name" unique="suffix">website</data> <data key="code" unique="suffix">website</data> <data key="sort_order">2</data> + <data key="store_action">add</data> + <data key="store_type">website</data> + <data key="website_id">null</data> </entity> <entity name="SecondWebsite" type="website"> <data key="name" unique="suffix">Second Website </data> diff --git a/app/code/Magento/Store/Test/Mftf/Metadata/product_website_link-meta.xml b/app/code/Magento/Store/Test/Mftf/Metadata/product_website_link-meta.xml new file mode 100644 index 0000000000000..ca0725f86c289 --- /dev/null +++ b/app/code/Magento/Store/Test/Mftf/Metadata/product_website_link-meta.xml @@ -0,0 +1,18 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<operations xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:DataGenerator/etc/dataOperation.xsd"> + <operation name="ProductWebsiteLink" dataType="product_website_link" type="create" auth="adminOauth" url="/V1/products/{sku}/websites" method="POST"> + <contentType>application/json</contentType> + <object dataType="product_website_link" key="productWebsiteLink"> + <field key="sku">string</field> + <field key="websiteId">integer</field> + </object> + </operation> +</operations> diff --git a/app/code/Magento/Store/Test/Mftf/Page/StorefrontStoreHomePage.xml b/app/code/Magento/Store/Test/Mftf/Page/StorefrontStoreHomePage.xml new file mode 100644 index 0000000000000..0cf1cffceac71 --- /dev/null +++ b/app/code/Magento/Store/Test/Mftf/Page/StorefrontStoreHomePage.xml @@ -0,0 +1,12 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<pages xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:mftf:Page/etc/PageObject.xsd"> + <page name="StorefrontStoreHomePage" url="/{{store_view}}/" area="storefront" module="Magento_Store" parameterized="true"> + <section name="StorefrontHeaderSection"/> + </page> +</pages> \ No newline at end of file diff --git a/app/code/Magento/Store/Test/Mftf/Section/AdminNewStoreViewActionsSection.xml b/app/code/Magento/Store/Test/Mftf/Section/AdminNewStoreViewActionsSection.xml index a3b5d1e616319..faffc69dc6975 100644 --- a/app/code/Magento/Store/Test/Mftf/Section/AdminNewStoreViewActionsSection.xml +++ b/app/code/Magento/Store/Test/Mftf/Section/AdminNewStoreViewActionsSection.xml @@ -5,7 +5,7 @@ * See COPYING.txt for license details. */ --> -<sections xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="../../../../../../../dev/tests/acceptance/vendor/magento/magento2-functional-testing-framework/src/Magento/FunctionalTestingFramework/Page/etc/SectionObject.xsd"> +<sections xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:mftf:Page/etc/SectionObject.xsd"> <section name="AdminNewStoreViewActionsSection"> <element name="backButton" type="button" selector="#back" timeout="30"/> <element name="delete" type="button" selector="#delete" timeout="30"/> diff --git a/app/code/Magento/Store/Test/Unit/Url/Plugin/RouteParamsResolverTest.php b/app/code/Magento/Store/Test/Unit/Url/Plugin/RouteParamsResolverTest.php index f31acd40a2e69..9b83714166b12 100644 --- a/app/code/Magento/Store/Test/Unit/Url/Plugin/RouteParamsResolverTest.php +++ b/app/code/Magento/Store/Test/Unit/Url/Plugin/RouteParamsResolverTest.php @@ -80,7 +80,7 @@ public function testBeforeSetRouteParamsScopeInParams() $routeParamsResolverMock->expects($this->once())->method('setScope')->with($storeCode); $routeParamsResolverMock->expects($this->once())->method('getScope')->willReturn($storeCode); - $this->queryParamsResolverMock->expects($this->never())->method('setQueryParam'); + $this->queryParamsResolverMock->expects($this->any())->method('setQueryParam'); $this->model->beforeSetRouteParams( $routeParamsResolverMock, @@ -113,7 +113,7 @@ public function testBeforeSetRouteParamsScopeUseStoreInUrl() $routeParamsResolverMock->expects($this->once())->method('setScope')->with($storeCode); $routeParamsResolverMock->expects($this->once())->method('getScope')->willReturn($storeCode); - $this->queryParamsResolverMock->expects($this->once())->method('setQueryParam')->with('___store', $storeCode); + $this->queryParamsResolverMock->expects($this->never())->method('setQueryParam')->with('___store', $storeCode); $this->model->beforeSetRouteParams( $routeParamsResolverMock, @@ -178,7 +178,7 @@ public function testBeforeSetRouteParamsNoScopeInParams() $routeParamsResolverMock->expects($this->never())->method('setScope'); $routeParamsResolverMock->expects($this->once())->method('getScope')->willReturn(false); - $this->queryParamsResolverMock->expects($this->once())->method('setQueryParam')->with('___store', $storeCode); + $this->queryParamsResolverMock->expects($this->never())->method('setQueryParam')->with('___store', $storeCode); $this->model->beforeSetRouteParams( $routeParamsResolverMock, diff --git a/app/code/Magento/Store/Url/Plugin/RouteParamsResolver.php b/app/code/Magento/Store/Url/Plugin/RouteParamsResolver.php index 468352af78cbc..9c9d1e6023af0 100644 --- a/app/code/Magento/Store/Url/Plugin/RouteParamsResolver.php +++ b/app/code/Magento/Store/Url/Plugin/RouteParamsResolver.php @@ -78,7 +78,7 @@ public function beforeSetRouteParams( $storeCode ); - if ($useStoreInUrl && !$this->storeManager->hasSingleStore()) { + if (!$useStoreInUrl && !$this->storeManager->hasSingleStore()) { $this->queryParamsResolver->setQueryParam('___store', $storeCode); } } diff --git a/app/code/Magento/Store/etc/di.xml b/app/code/Magento/Store/etc/di.xml index 3d740bfee2093..83f891d06647d 100644 --- a/app/code/Magento/Store/etc/di.xml +++ b/app/code/Magento/Store/etc/di.xml @@ -232,6 +232,7 @@ <type name="Magento\Framework\App\ScopeResolverPool"> <arguments> <argument name="scopeResolvers" xsi:type="array"> + <item name="default" xsi:type="object">Magento\Framework\App\ScopeResolver</item> <item name="store" xsi:type="object">Magento\Store\Model\Resolver\Store</item> <item name="stores" xsi:type="object">Magento\Store\Model\Resolver\Store</item> <item name="group" xsi:type="object">Magento\Store\Model\Resolver\Group</item> diff --git a/app/code/Magento/Store/etc/frontend/di.xml b/app/code/Magento/Store/etc/frontend/di.xml index c39d5df863939..917aedad3d960 100644 --- a/app/code/Magento/Store/etc/frontend/di.xml +++ b/app/code/Magento/Store/etc/frontend/di.xml @@ -9,7 +9,7 @@ <type name="Magento\Framework\App\FrontController"> <plugin name="requestPreprocessor" type="Magento\Store\App\FrontController\Plugin\RequestPreprocessor" sortOrder="50"/> </type> - <type name="Magento\Framework\App\Action\Action"> + <type name="Magento\Framework\App\Action\AbstractAction"> <plugin name="contextPlugin" type="Magento\Store\App\Action\Plugin\Context" sortOrder="10"/> </type> <type name="Magento\Framework\App\RouterList" shared="true"> diff --git a/app/code/Magento/Swatches/Controller/Ajax/Media.php b/app/code/Magento/Swatches/Controller/Ajax/Media.php index 079ba8f897127..d26080635c456 100644 --- a/app/code/Magento/Swatches/Controller/Ajax/Media.php +++ b/app/code/Magento/Swatches/Controller/Ajax/Media.php @@ -44,6 +44,7 @@ public function __construct( * Get product media for specified configurable product variation * * @return string + * @throws \Magento\Framework\Exception\LocalizedException */ public function execute() { diff --git a/app/code/Magento/Swatches/Helper/Data.php b/app/code/Magento/Swatches/Helper/Data.php index 6f751068d543a..ae35f5203dd73 100644 --- a/app/code/Magento/Swatches/Helper/Data.php +++ b/app/code/Magento/Swatches/Helper/Data.php @@ -6,6 +6,7 @@ namespace Magento\Swatches\Helper; +use Magento\Catalog\Api\Data\ProductAttributeMediaGalleryEntryInterface; use Magento\Catalog\Api\Data\ProductInterface as Product; use Magento\Catalog\Api\ProductRepositoryInterface; use Magento\Catalog\Helper\Image; @@ -311,37 +312,67 @@ private function addFilterByParent(ProductCollection $productCollection, $parent * ] * @param ModelProduct $product * @return array + * @throws \Magento\Framework\Exception\LocalizedException */ - public function getProductMediaGallery(ModelProduct $product) + public function getProductMediaGallery(ModelProduct $product): array { $baseImage = null; $gallery = []; $mediaGallery = $product->getMediaGalleryEntries(); + /** @var ProductAttributeMediaGalleryEntryInterface $mediaEntry */ foreach ($mediaGallery as $mediaEntry) { if ($mediaEntry->isDisabled()) { continue; } - if (in_array('image', $mediaEntry->getTypes(), true)) { - $baseImage = $mediaEntry->getFile(); + if (!$baseImage || $this->isMainImage($mediaEntry)) { + $baseImage = $mediaEntry; } elseif (!$baseImage) { $baseImage = $mediaEntry->getFile(); } - $gallery[$mediaEntry->getId()] = $this->getAllSizeImages($product, $mediaEntry->getFile()); + $gallery[$mediaEntry->getId()] = $this->collectImageData($product, $mediaEntry); } if (!$baseImage) { return []; } - $resultGallery = $this->getAllSizeImages($product, $baseImage); + $resultGallery = $this->collectImageData($product, $baseImage); $resultGallery['gallery'] = $gallery; return $resultGallery; } + /** + * Checks if image is main image in gallery + * + * @param ProductAttributeMediaGalleryEntryInterface $mediaEntry + * @return bool + */ + private function isMainImage(ProductAttributeMediaGalleryEntryInterface $mediaEntry): bool + { + return in_array('image', $mediaEntry->getTypes(), true); + } + + /** + * Returns image data for swatches + * + * @param ModelProduct $product + * @param ProductAttributeMediaGalleryEntryInterface $mediaEntry + * @return array + */ + private function collectImageData( + ModelProduct $product, + ProductAttributeMediaGalleryEntryInterface $mediaEntry + ): array { + $image = $this->getAllSizeImages($product, $mediaEntry->getFile()); + $image[ProductAttributeMediaGalleryEntryInterface::POSITION] = $mediaEntry->getPosition(); + $image['isMain'] = $this->isMainImage($mediaEntry); + return $image; + } + /** * @param ModelProduct $product * @param string $imageFile diff --git a/app/code/Magento/Swatches/Test/Mftf/Test/StorefrontSwatchProductWithFileCustomOptionTest.xml b/app/code/Magento/Swatches/Test/Mftf/Test/StorefrontSwatchProductWithFileCustomOptionTest.xml index 1786b9bad236d..8f13860f75ad1 100644 --- a/app/code/Magento/Swatches/Test/Mftf/Test/StorefrontSwatchProductWithFileCustomOptionTest.xml +++ b/app/code/Magento/Swatches/Test/Mftf/Test/StorefrontSwatchProductWithFileCustomOptionTest.xml @@ -70,7 +70,6 @@ <!--Try invalid file--> <attachFile selector="{{StorefrontProductInfoMainSection.addLinkFileUploadFile(ProductOptionFile.title)}}" userInput="lorem_ipsum.docx" stepKey="attachInvalidFile"/> <click selector="{{StorefrontProductPageSection.addToCartBtn}}" stepKey="addToCartInvalidFile"/> - <waitForPageLoad time="30" stepKey="waitForAddToCartWithError"/> <waitForElementVisible selector="{{StorefrontProductPageSection.alertMessage}}" stepKey="waitForErrorMessageInvalidFile"/> <see selector="{{StorefrontProductPageSection.messagesBlock}}" userInput="The file 'lorem_ipsum.docx' for '{{ProductOptionFile.title}}' has an invalid extension." stepKey="seeMessageInvalidFile"/> <!--Swatch remains selected--> diff --git a/app/code/Magento/Swatches/view/frontend/web/js/swatch-renderer.js b/app/code/Magento/Swatches/view/frontend/web/js/swatch-renderer.js index 5e4c0a2c84f14..1145d3acc11ed 100644 --- a/app/code/Magento/Swatches/view/frontend/web/js/swatch-renderer.js +++ b/app/code/Magento/Swatches/view/frontend/web/js/swatch-renderer.js @@ -667,10 +667,9 @@ define([ /** * Load media gallery using ajax or json config. * - * @param {String|undefined} eventName * @private */ - _loadMedia: function (eventName) { + _loadMedia: function () { var $main = this.inProductList ? this.element.parents('.product-item-info') : this.element.parents('.column.main'), @@ -685,10 +684,21 @@ define([ images = this.options.mediaGalleryInitial; } - this.updateBaseImage(images, $main, !this.inProductList, eventName); + this.updateBaseImage(this._sortImages(images), $main, !this.inProductList); } }, + /** + * Sorting images array + * + * @private + */ + _sortImages: function (images) { + return _.sortBy(images, function (image) { + return image.position; + }); + }, + /** * Event for swatch options * @@ -1020,14 +1030,10 @@ define([ _.each(allowedProducts, function (allowedProduct) { optionFinalPrice = parseFloat(optionPrices[allowedProduct].finalPrice.amount); - if (_.isEmpty(product)) { + if (_.isEmpty(product) || optionFinalPrice < optionMinPrice) { optionMinPrice = optionFinalPrice; product = allowedProduct; } - - if (optionFinalPrice < optionMinPrice) { - product = allowedProduct; - } }, this); return product; @@ -1252,9 +1258,6 @@ define([ dataMergeStrategy: this.options.gallerySwitchStrategy }); } - - gallery.first(); - } else if (justAnImage && justAnImage.img) { context.find('.product-image-photo').attr('src', justAnImage.img); } diff --git a/app/code/Magento/Tax/Model/ResourceModel/Report/Tax/Createdat.php b/app/code/Magento/Tax/Model/ResourceModel/Report/Tax/Createdat.php index ebf699db552d6..67477d7056bae 100644 --- a/app/code/Magento/Tax/Model/ResourceModel/Report/Tax/Createdat.php +++ b/app/code/Magento/Tax/Model/ResourceModel/Report/Tax/Createdat.php @@ -11,6 +11,9 @@ */ namespace Magento\Tax\Model\ResourceModel\Report\Tax; +/** + * Class for tax report resource model with aggregation by created at. + */ class Createdat extends \Magento\Reports\Model\ResourceModel\Report\AbstractReport { /** @@ -84,7 +87,7 @@ protected function _aggregateByOrder($aggregationField, $from, $to) 'order_status' => 'e.status', 'percent' => 'MAX(tax.' . $connection->quoteIdentifier('percent') . ')', 'orders_count' => 'COUNT(DISTINCT e.entity_id)', - 'tax_base_amount_sum' => 'SUM(tax.base_amount * e.base_to_global_rate)', + 'tax_base_amount_sum' => 'SUM(tax.base_real_amount * e.base_to_global_rate)', ]; $select = $connection->select()->from( diff --git a/app/code/Magento/Tax/Test/Mftf/ActionGroup/AdminTaxActionGroup.xml b/app/code/Magento/Tax/Test/Mftf/ActionGroup/AdminTaxActionGroup.xml new file mode 100644 index 0000000000000..adb120c78126b --- /dev/null +++ b/app/code/Magento/Tax/Test/Mftf/ActionGroup/AdminTaxActionGroup.xml @@ -0,0 +1,45 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> + <!-- Add new Tax Rule with custom tax Rate and product Tax Class --> + <actionGroup name="AddTaxRule"> + <arguments> + <argument name="taxRuleName" type="string"/> + <argument name="taxRate"/> + <argument name="productTaxClass"/> + </arguments> + <amOnPage url="{{AdminTaxRuleNewPage.url}}" stepKey="goToTaxRulePage"/> + <fillField selector="{{AdminTaxRuleEditFormSection.ruleName}}" userInput="{{taxRuleName}}" stepKey="fillTaxRuleName"/> + <click selector="{{AdminTaxRuleEditFormSection.selectTaxRate(taxRate.code)}}" stepKey="selectTaxRate"/> + <conditionalClick selector="{{AdminTaxRuleEditFormSection.additionalSettings}}" dependentSelector="{{AdminTaxRuleEditFormSection.additionalSettings}}" visible="true" stepKey="clickAdditionalSettings"/> + <scrollTo selector="{{AdminTaxRuleEditFormSection.selectProductTaxClass(productTaxClass.class_name)}}" stepKey="scrollToProductTaxClass"/> + <click selector="{{AdminTaxRuleEditFormSection.selectProductTaxClass(productTaxClass.class_name)}}" stepKey="selectProductTaxClass"/> + <click selector="{{AdminMainActionsSection.save}}" stepKey="saveTaxRule"/> + <see selector="{{AdminMessagesSection.success}}" userInput="You saved the tax rule." stepKey="seeSuccessMessage"/> + </actionGroup> + + <actionGroup name="DeleteTaxRule"> + <arguments> + <argument name="taxRuleName" type="string"/> + </arguments> + <amOnPage url="{{AdminTaxRulesGridPage.url}}" stepKey="loadTaxRulePage"/> + <click selector="{{AdminDataGridHeaderSection.clearFilters}}" stepKey="clearFilterBefore"/> + <fillField selector="{{AdminTaxRulesGridHeaderSection.taxRuleCodeFilter}}" userInput="{{taxRuleName}}" stepKey="fillTaxRuleName"/> + <click selector="{{AdminDataGridHeaderSection.applyFilters}}" stepKey="clickSearchButton"/> + <click selector="{{AdminGridTableSection.row('1')}}" stepKey="openTaxRule"/> + <waitForPageLoad stepKey="waitForTaxRulePage"/> + <click selector="{{AdminMainActionsSection.delete}}" stepKey="clickDeleteTaxRule"/> + <waitForElementVisible selector="{{AdminConfirmationModalSection.ok}}" stepKey="waitForPageLoadAfterClickingDelete"/> + <click selector="{{AdminConfirmationModalSection.ok}}" stepKey="acceptDelete"/> + <see selector="{{AdminMessagesSection.success}}" userInput="The tax rule has been deleted." stepKey="seeDeleteSuccessMessage"/> + <click selector="{{AdminDataGridHeaderSection.clearFilters}}" stepKey="clearFilterAfter"/> + </actionGroup> + +</actionGroups> diff --git a/app/code/Magento/Tax/Test/Mftf/Data/TaxClassData.xml b/app/code/Magento/Tax/Test/Mftf/Data/TaxClassData.xml new file mode 100644 index 0000000000000..b588b4176b6e8 --- /dev/null +++ b/app/code/Magento/Tax/Test/Mftf/Data/TaxClassData.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="ProductTaxClass" type="taxClass"> + <data key="class_name" unique="suffix">TaxClass</data> + <data key="class_type">PRODUCT</data> + </entity> + <entity name="ProductTaxClassGetter" type="taxClass"> + <var key="class_id" entityKey="return" entityType="taxClass"/> + </entity> +</entities> diff --git a/app/code/Magento/Tax/Test/Mftf/Data/TaxConfigData.xml b/app/code/Magento/Tax/Test/Mftf/Data/TaxConfigData.xml index 5edabc0826ddc..534c575f90e1a 100644 --- a/app/code/Magento/Tax/Test/Mftf/Data/TaxConfigData.xml +++ b/app/code/Magento/Tax/Test/Mftf/Data/TaxConfigData.xml @@ -142,4 +142,17 @@ <entity name="EmptyField" type="taxPostCodeEmpty"> <data key="value"/> </entity> + <!--Tax Class for Shipping--> + <entity name="TaxClassForShippingConfig" type="tax_config_state"> + <requiredEntity type="shipping_tax_class">ShippingTaxClassTaxableGoods</requiredEntity> + </entity> + <entity name="DefaultTaxClassForShippingConfig" type="tax_config_state"> + <requiredEntity type="shipping_tax_class">ShippingTaxClassNone</requiredEntity> + </entity> + <entity name="ShippingTaxClassNone" type="shipping_tax_class"> + <data key="value">0</data> + </entity> + <entity name="ShippingTaxClassTaxableGoods" type="shipping_tax_class"> + <data key="value">2</data> + </entity> </entities> diff --git a/app/code/Magento/Tax/Test/Mftf/Data/TaxRateData.xml b/app/code/Magento/Tax/Test/Mftf/Data/TaxRateData.xml new file mode 100644 index 0000000000000..a99c51ea6e5f1 --- /dev/null +++ b/app/code/Magento/Tax/Test/Mftf/Data/TaxRateData.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="TexasTaxRate" type="taxRate"> + <data key="code" unique="suffix">USTEXAS</data> + <data key="tax_country_id">US</data> + <data key="tax_region_id">57</data> + <data key="region_name">TX</data> + <data key="tax_postcode">*</data> + <data key="rate">1</data> + </entity> + <entity name="AustinTaxRate" type="taxRate"> + <data key="code" unique="suffix">USTEXASAUSTIN</data> + <data key="tax_country_id">US</data> + <data key="tax_region_id">57</data> + <data key="region_name">TX</data> + <data key="tax_postcode">78729</data> + <data key="rate">5</data> + </entity> +</entities> diff --git a/app/code/Magento/Tax/Test/Mftf/Metadata/tax_class-meta.xml b/app/code/Magento/Tax/Test/Mftf/Metadata/tax_class-meta.xml new file mode 100644 index 0000000000000..b514c3feb6fe3 --- /dev/null +++ b/app/code/Magento/Tax/Test/Mftf/Metadata/tax_class-meta.xml @@ -0,0 +1,24 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<operations xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:DataGenerator/etc/dataOperation.xsd"> + <operation name="CreateTaxClass" dataType="taxClass" type="create" auth="adminOauth" url="/V1/taxClasses" method="POST"> + <contentType>application/json</contentType> + <object key="taxClass" dataType="taxClass"> + <field key="class_name" required="true">string</field> + <field key="class_type" required="true">string</field> + </object> + </operation> + <operation name="GetTaxClass" dataType="taxClass" type="get" auth="adminOauth" url="/V1/taxClasses/{return}" method="GET"> + <contentType>application/json</contentType> + </operation> + <operation name="DeleteTaxClass" dataType="taxClass" type="delete" auth="adminOauth" url="/V1/taxClasses/{return}" method="DELETE"> + <contentType>application/json</contentType> + </operation> +</operations> diff --git a/app/code/Magento/Tax/Test/Mftf/Metadata/tax_config-meta.xml b/app/code/Magento/Tax/Test/Mftf/Metadata/tax_config-meta.xml index af7c0dc5e16a7..ebe4790543fdc 100644 --- a/app/code/Magento/Tax/Test/Mftf/Metadata/tax_config-meta.xml +++ b/app/code/Magento/Tax/Test/Mftf/Metadata/tax_config-meta.xml @@ -7,8 +7,15 @@ --> <operations xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="../../../../../../../dev/tests/acceptance/vendor/magento/magento2-functional-testing-framework/src/Magento/FunctionalTestingFramework/DataGenerator/etc/dataOperation.xsd"> - <operation name="CreateTaxConfigDefaultsTaxDestination" dataType="tax_config_state" type="create" auth="adminFormKey" url="/admin/system_config/save/section/tax/" method="POST"> + <operation name="CreateTaxConfigDefaultsTaxDestination" dataType="tax_config_state" type="create" auth="adminFormKey" url="/admin/system_config/save/section/tax/" method="POST" successRegex="/messages-message-success/"> <object key="groups" dataType="tax_config_state"> + <object key="classes" dataType="tax_config_state"> + <object key="fields" dataType="tax_config_state"> + <object key="shipping_tax_class" dataType="shipping_tax_class"> + <field key="value">integer</field> + </object> + </object> + </object> <object key="calculation" dataType="tax_config_state"> <object key="fields" dataType="tax_config_state"> <object key="algorithm" dataType="algorithm"> diff --git a/app/code/Magento/Tax/Test/Mftf/Metadata/tax_rate-meta.xml b/app/code/Magento/Tax/Test/Mftf/Metadata/tax_rate-meta.xml new file mode 100644 index 0000000000000..1357aa9c831d1 --- /dev/null +++ b/app/code/Magento/Tax/Test/Mftf/Metadata/tax_rate-meta.xml @@ -0,0 +1,25 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<operations xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:DataGenerator/etc/dataOperation.xsd"> + <operation name="CreateTaxRate" dataType="taxRate" type="create" auth="adminOauth" url="/V1/taxRates" method="POST"> + <contentType>application/json</contentType> + <object key="taxRate" dataType="taxRate"> + <field key="code" required="true">string</field> + <field key="tax_country_id" required="true">string</field> + <field key="tax_region_id" required="true">integer</field> + <field key="region_name" required="true">string</field> + <field key="tax_postcode" required="true">string</field> + <field key="rate" required="true">number</field> + </object> + </operation> + <operation name="DeleteTaxRate" dataType="taxRate" type="delete" auth="adminOauth" url="/V1/taxRates/{id}" method="DELETE"> + <contentType>application/json</contentType> + </operation> +</operations> diff --git a/app/code/Magento/Tax/Test/Mftf/Page/AdminTaxRuleNewPage.xml b/app/code/Magento/Tax/Test/Mftf/Page/AdminTaxRuleNewPage.xml new file mode 100644 index 0000000000000..a97567a438108 --- /dev/null +++ b/app/code/Magento/Tax/Test/Mftf/Page/AdminTaxRuleNewPage.xml @@ -0,0 +1,15 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<pages xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Page/etc/PageObject.xsd"> + <page name="AdminTaxRuleNewPage" url="tax/rule/new/" area="admin" module="Magento_Tax"> + <section name="AdminTaxRuleEditFormSection"/> + </page> +</pages> + diff --git a/app/code/Magento/Tax/Test/Mftf/Page/AdminTaxRulesGridPage.xml b/app/code/Magento/Tax/Test/Mftf/Page/AdminTaxRulesGridPage.xml new file mode 100644 index 0000000000000..0672c74c3d358 --- /dev/null +++ b/app/code/Magento/Tax/Test/Mftf/Page/AdminTaxRulesGridPage.xml @@ -0,0 +1,15 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<pages xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Page/etc/PageObject.xsd"> + <page name="AdminTaxRulesGridPage" url="tax/rule/" area="admin" module="Magento_Tax"> + <section name="AdminTaxRulesGridHeaderSection"/> + </page> +</pages> + diff --git a/app/code/Magento/Tax/Test/Mftf/Section/AdminTaxRuleEditFormSection.xml b/app/code/Magento/Tax/Test/Mftf/Section/AdminTaxRuleEditFormSection.xml new file mode 100644 index 0000000000000..de93d74e31111 --- /dev/null +++ b/app/code/Magento/Tax/Test/Mftf/Section/AdminTaxRuleEditFormSection.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="AdminTaxRuleEditFormSection"> + <element name="ruleName" type="input" selector="#edit_form #code"/> + <element name="selectTaxRate" type="select" selector="//*[@id='tax_rate']/following-sibling::section[contains(@class, 'mselect-list')]//span[text()='{{taxRate}}']" parameterized="true" timeout="30"/> + <element name="additionalSettings" type="button" selector="#details-summarybase_fieldset"/> + <element name="selectProductTaxClass" type="select" selector="//*[@id='tax_product_class']/following-sibling::section[contains(@class, 'mselect-list')]//span[text()='{{taxClass}}']" parameterized="true" timeout="30"/> + </section> +</sections> diff --git a/app/code/Magento/Tax/Test/Mftf/Section/AdminTaxRulesGridSection.xml b/app/code/Magento/Tax/Test/Mftf/Section/AdminTaxRulesGridSection.xml new file mode 100644 index 0000000000000..5988965b64cad --- /dev/null +++ b/app/code/Magento/Tax/Test/Mftf/Section/AdminTaxRulesGridSection.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="AdminTaxRulesGridHeaderSection"> + <element name="taxRuleCodeFilter" type="input" selector="#taxRuleGrid_filter_code"/> + </section> +</sections> diff --git a/app/code/Magento/Tax/Test/Mftf/Test/StorefrontTaxInformationInShoppingCartForCustomerPhysicalQuoteTest.xml b/app/code/Magento/Tax/Test/Mftf/Test/StorefrontTaxInformationInShoppingCartForCustomerPhysicalQuoteTest.xml index 3c6953f08e8d7..3fd06016624d1 100644 --- a/app/code/Magento/Tax/Test/Mftf/Test/StorefrontTaxInformationInShoppingCartForCustomerPhysicalQuoteTest.xml +++ b/app/code/Magento/Tax/Test/Mftf/Test/StorefrontTaxInformationInShoppingCartForCustomerPhysicalQuoteTest.xml @@ -7,7 +7,7 @@ --> <tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" - xsi:noNamespaceSchemaLocation="../../../../../../../dev/tests/acceptance/vendor/magento/magento2-functional-testing-framework/src/Magento/FunctionalTestingFramework/Test/etc/testSchema.xsd"> + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> <test name="StorefrontTaxInformationInShoppingCartForCustomerPhysicalQuoteTest"> <annotations> <features value="Tax information in shopping cart for Customer with default addresses (physical quote)"/> @@ -62,6 +62,7 @@ <actionGroup ref="AdminResetProductGridToDefaultViewActionGroup" stepKey="resetGridToDefaultKeywordSearch"/> </before> <after> + <actionGroup ref="CustomerLogoutStorefrontActionGroup" stepKey="customerLogout"/> <deleteData createDataKey="createTaxRule" stepKey="deleteTaxRule"/> <deleteData createDataKey="createProductFPTAttribute" stepKey="deleteProductFPTAttribute"/> <createData entity="DefaultTaxConfig" stepKey="defaultTaxConfiguration"/> @@ -87,9 +88,13 @@ <actionGroup ref="StorefrontViewAndEditCartFromMiniCartActionGroup" stepKey="goToShoppingCartFromMinicart"/> <!-- Step 4: Open Estimate Shipping and Tax section --> <conditionalClick selector="{{StorefrontCheckoutCartSummarySection.estimateShippingAndTax}}" dependentSelector="{{StorefrontCheckoutCartSummarySection.country}}" visible="false" stepKey="expandEstimateShippingandTax" /> - <see selector="{{StorefrontCheckoutCartSummarySection.country}}" userInput="$$createCustomer.country_id$$" stepKey="checkCustomerCountry" /> - <see selector="{{StorefrontCheckoutCartSummarySection.region}}" userInput="$$createCustomer.state$$" stepKey="checkCustomerRegion" /> - <see selector="{{StorefrontCheckoutCartSummarySection.postcode}}" userInput="$$createCustomer.postcode$$" stepKey="checkCustomerPostcode" /> + <seeOptionIsSelected selector="{{StorefrontCheckoutCartSummarySection.country}}" userInput="{{US_Address_CA.country}}" stepKey="checkCustomerCountry" /> + <seeOptionIsSelected selector="{{StorefrontCheckoutCartSummarySection.region}}" userInput="{{US_Address_CA.state}}" stepKey="checkCustomerRegion" /> + <grabValueFrom selector="{{StorefrontCheckoutCartSummarySection.postcode}}" stepKey="grabTextPostCode"/> + <assertEquals message="Customer postcode is invalid" stepKey="checkCustomerPostcode"> + <expectedResult type="string">{{US_Address_CA.postcode}}</expectedResult> + <actualResult type="variable">grabTextPostCode</actualResult> + </assertEquals> <see selector="{{StorefrontCheckoutCartSummarySection.amountFPT}}" userInput="$10" stepKey="checkAmountFPTCA" /> <see selector="{{StorefrontCheckoutCartSummarySection.taxAmount}}" userInput="$0.83" stepKey="checkTaxAmountCA" /> <scrollTo selector="{{StorefrontCheckoutCartSummarySection.taxSummary}}" stepKey="scrollToTaxSummary" /> diff --git a/app/code/Magento/Tax/Test/Mftf/Test/StorefrontTaxInformationInShoppingCartForCustomerVirtualQuoteTest.xml b/app/code/Magento/Tax/Test/Mftf/Test/StorefrontTaxInformationInShoppingCartForCustomerVirtualQuoteTest.xml index fbb44807c8b14..f26b6d2747e09 100644 --- a/app/code/Magento/Tax/Test/Mftf/Test/StorefrontTaxInformationInShoppingCartForCustomerVirtualQuoteTest.xml +++ b/app/code/Magento/Tax/Test/Mftf/Test/StorefrontTaxInformationInShoppingCartForCustomerVirtualQuoteTest.xml @@ -7,7 +7,7 @@ --> <tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" - xsi:noNamespaceSchemaLocation="../../../../../../../dev/tests/acceptance/vendor/magento/magento2-functional-testing-framework/src/Magento/FunctionalTestingFramework/Test/etc/testSchema.xsd"> + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> <test name="StorefrontTaxInformationInShoppingCartForCustomerVirtualQuoteTest"> <annotations> <features value="Tax information in shopping cart for Customer with default addresses (virtual quote)"/> @@ -37,6 +37,7 @@ <createData entity="Simple_US_NY_Customer" stepKey="createCustomer"/> </before> <after> + <actionGroup ref="CustomerLogoutStorefrontActionGroup" stepKey="customerLogout"/> <deleteData createDataKey="createTaxRule" stepKey="deleteTaxRule"/> <deleteData createDataKey="createProductFPTAttribute" stepKey="deleteProductFPTAttribute"/> <createData entity="DefaultTaxConfig" stepKey="defaultTaxConfiguration"/> @@ -59,9 +60,13 @@ <actionGroup ref="StorefrontViewAndEditCartFromMiniCartActionGroup" stepKey="goToShoppingCartFromMinicart"/> <!-- Step 4: Open Estimate Shipping and Tax section --> <conditionalClick selector="{{StorefrontCheckoutCartSummarySection.estimateShippingAndTax}}" dependentSelector="{{StorefrontCheckoutCartSummarySection.country}}" visible="false" stepKey="expandEstimateShippingandTax" /> - <see selector="{{StorefrontCheckoutCartSummarySection.country}}" userInput="$$createCustomer.country_id$$" stepKey="checkCustomerCountry" /> - <see selector="{{StorefrontCheckoutCartSummarySection.region}}" userInput="$$createCustomer.state$$" stepKey="checkCustomerRegion" /> - <see selector="{{StorefrontCheckoutCartSummarySection.postcode}}" userInput="$$createCustomer.postcode$$" stepKey="checkCustomerPostcode" /> + <seeOptionIsSelected selector="{{StorefrontCheckoutCartSummarySection.country}}" userInput="{{US_Address_NY.country}}" stepKey="checkCustomerCountry" /> + <seeOptionIsSelected selector="{{StorefrontCheckoutCartSummarySection.region}}" userInput="{{US_Address_NY.state}}" stepKey="checkCustomerRegion" /> + <grabValueFrom selector="{{StorefrontCheckoutCartSummarySection.postcode}}" stepKey="grabTextPostCode"/> + <assertEquals message="Customer postcode is invalid" stepKey="checkCustomerPostcode"> + <expectedResult type="string">{{US_Address_NY.postcode}}</expectedResult> + <actualResult type="variable">grabTextPostCode</actualResult> + </assertEquals> <scrollTo selector="{{StorefrontCheckoutCartSummarySection.taxSummary}}" stepKey="scrollToTaxSummary" /> <click selector="{{StorefrontCheckoutCartSummarySection.taxSummary}}" stepKey="expandTaxSummary"/> <see selector="{{StorefrontCheckoutCartSummarySection.rate}}" userInput="US-NY-*-Rate 1 (8.375%)" stepKey="checkRateNY" /> diff --git a/app/code/Magento/Tax/view/base/web/js/price/adjustment.js b/app/code/Magento/Tax/view/base/web/js/price/adjustment.js index 9af15f84562f4..a17d130d9282a 100644 --- a/app/code/Magento/Tax/view/base/web/js/price/adjustment.js +++ b/app/code/Magento/Tax/view/base/web/js/price/adjustment.js @@ -62,7 +62,7 @@ define([ }, /** - * Set price taax type. + * Set price tax type. * * @param {String} priceType * @return {Object} diff --git a/app/code/Magento/Theme/Block/Html/Header/Logo.php b/app/code/Magento/Theme/Block/Html/Header/Logo.php index 0a0e71f44ba32..b51f624c20339 100644 --- a/app/code/Magento/Theme/Block/Html/Header/Logo.php +++ b/app/code/Magento/Theme/Block/Html/Header/Logo.php @@ -43,6 +43,10 @@ public function __construct( /** * Check if current url is url for home page * + * @deprecated This function is no longer used. It was previously used by + * Magento/Theme/view/frontend/templates/html/header/logo.phtml + * to check if the logo should be clickable on the homepage. + * * @return bool */ public function isHomePage() diff --git a/app/code/Magento/Theme/Block/Html/Topmenu.php b/app/code/Magento/Theme/Block/Html/Topmenu.php index 47f88c65acb80..baf2f3878ad9b 100644 --- a/app/code/Magento/Theme/Block/Html/Topmenu.php +++ b/app/code/Magento/Theme/Block/Html/Topmenu.php @@ -78,6 +78,7 @@ protected function getCacheLifetime() * @param string $childrenWrapClass * @param int $limit * @return string + * @SuppressWarnings(PHPMD.RequestAwareBlockMethod) */ public function getHtml($outermostClass = '', $childrenWrapClass = '', $limit = 0) { @@ -361,19 +362,6 @@ public function getIdentities() return $this->identities; } - /** - * Get cache key informative items - * - * @return array - * @since 100.1.0 - */ - public function getCacheKeyInfo() - { - $keyInfo = parent::getCacheKeyInfo(); - $keyInfo[] = $this->getUrl('*/*/*', ['_current' => true, '_query' => '']); - return $keyInfo; - } - /** * Get tags array for saving cache * diff --git a/app/code/Magento/Theme/Test/Mftf/Section/StorefrontHeaderSection.xml b/app/code/Magento/Theme/Test/Mftf/Section/StorefrontHeaderSection.xml new file mode 100644 index 0000000000000..a4088c7a4a0b7 --- /dev/null +++ b/app/code/Magento/Theme/Test/Mftf/Section/StorefrontHeaderSection.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="StorefrontHeaderSection"> + <element name="welcomeMessage" type="text" selector=".greet.welcome"/> + </section> +</sections> diff --git a/app/code/Magento/Theme/Test/Unit/Block/Html/TopmenuTest.php b/app/code/Magento/Theme/Test/Unit/Block/Html/TopmenuTest.php index 91c3ce47fc8b8..023c741492752 100644 --- a/app/code/Magento/Theme/Test/Unit/Block/Html/TopmenuTest.php +++ b/app/code/Magento/Theme/Test/Unit/Block/Html/TopmenuTest.php @@ -189,7 +189,6 @@ public function testGetCacheKeyInfo() $treeFactory = $this->createMock(\Magento\Framework\Data\TreeFactory::class); $topmenu = new Topmenu($this->context, $nodeFactory, $treeFactory); - $this->urlBuilder->expects($this->once())->method('getUrl')->with('*/*/*')->willReturn('123'); $this->urlBuilder->expects($this->once())->method('getBaseUrl')->willReturn('baseUrl'); $store = $this->getMockBuilder(\Magento\Store\Model\Store::class) ->disableOriginalConstructor() @@ -199,7 +198,7 @@ public function testGetCacheKeyInfo() $this->storeManager->expects($this->once())->method('getStore')->willReturn($store); $this->assertEquals( - ['BLOCK_TPL', '321', null, 'base_url' => 'baseUrl', 'template' => null, '123'], + ['BLOCK_TPL', '321', null, 'base_url' => 'baseUrl', 'template' => null], $topmenu->getCacheKeyInfo() ); } diff --git a/app/code/Magento/Theme/view/frontend/templates/html/header.phtml b/app/code/Magento/Theme/view/frontend/templates/html/header.phtml index 4395cab651de1..1103ae28741c6 100644 --- a/app/code/Magento/Theme/view/frontend/templates/html/header.phtml +++ b/app/code/Magento/Theme/view/frontend/templates/html/header.phtml @@ -15,11 +15,11 @@ $welcomeMessage = $block->getWelcome(); case 'welcome': ?> <li class="greet welcome" data-bind="scope: 'customer'"> <!-- ko if: customer().fullname --> - <span data-bind="text: new String('<?= $block->escapeHtml(__('Welcome, %1!', '%1')) ?>').replace('%1', customer().fullname)"> + <span class="logged-in" data-bind="text: new String('<?= $block->escapeHtml(__('Welcome, %1!', '%1')) ?>').replace('%1', customer().fullname)"> </span> <!-- /ko --> <!-- ko ifnot: customer().fullname --> - <span data-bind='html:"<?= $block->escapeHtml($welcomeMessage) ?>"'></span> + <span class="not-logged-in" data-bind='html:"<?= $block->escapeHtml($welcomeMessage) ?>"'></span> <?= $block->getBlockHtml('header.additional') ?> <!-- /ko --> </li> diff --git a/app/code/Magento/Theme/view/frontend/templates/html/header/logo.phtml b/app/code/Magento/Theme/view/frontend/templates/html/header/logo.phtml index 17f8d7c70f574..79b891b7e55e6 100644 --- a/app/code/Magento/Theme/view/frontend/templates/html/header/logo.phtml +++ b/app/code/Magento/Theme/view/frontend/templates/html/header/logo.phtml @@ -12,19 +12,11 @@ ?> <?php $storeName = $block->getThemeName() ? $block->getThemeName() : $block->getLogoAlt();?> <span data-action="toggle-nav" class="action nav-toggle"><span><?= /* @escapeNotVerified */ __('Toggle Nav') ?></span></span> -<?php if ($block->isHomePage()):?> - <strong class="logo"> -<?php else: ?> - <a class="logo" href="<?= $block->getUrl('') ?>" title="<?= /* @escapeNotVerified */ $storeName ?>"> -<?php endif ?> - <img src="<?= /* @escapeNotVerified */ $block->getLogoSrc() ?>" - title="<?= /* @escapeNotVerified */ $block->getLogoAlt() ?>" - alt="<?= /* @escapeNotVerified */ $block->getLogoAlt() ?>" - <?= $block->getLogoWidth() ? 'width="' . $block->getLogoWidth() . '"' : '' ?> - <?= $block->getLogoHeight() ? 'height="' . $block->getLogoHeight() . '"' : '' ?> - /> -<?php if ($block->isHomePage()):?> - </strong> -<?php else:?> - </a> -<?php endif?> +<a class="logo" href="<?= $block->getUrl('') ?>" title="<?= /* @escapeNotVerified */ $storeName ?>"> + <img src="<?= /* @escapeNotVerified */ $block->getLogoSrc() ?>" + title="<?= /* @escapeNotVerified */ $block->getLogoAlt() ?>" + alt="<?= /* @escapeNotVerified */ $block->getLogoAlt() ?>" + <?= $block->getLogoWidth() ? 'width="' . $block->getLogoWidth() . '"' : '' ?> + <?= $block->getLogoHeight() ? 'height="' . $block->getLogoHeight() . '"' : '' ?> + /> +</a> diff --git a/app/code/Magento/Ui/Component/Listing/Columns/Date.php b/app/code/Magento/Ui/Component/Listing/Columns/Date.php index 75cb9dfbc52f7..acdce73572173 100644 --- a/app/code/Magento/Ui/Component/Listing/Columns/Date.php +++ b/app/code/Magento/Ui/Component/Listing/Columns/Date.php @@ -11,6 +11,8 @@ use Magento\Framework\Stdlib\DateTime\TimezoneInterface; /** + * Date format column. + * * @api * @since 100.0.2 */ @@ -47,6 +49,28 @@ public function __construct( parent::__construct($context, $uiComponentFactory, $components, $data); } + /** + * @inheritdoc + */ + public function prepare() + { + $config = $this->getData('config'); + $config['filter'] = [ + 'filterType' => 'dateRange', + 'templates' => [ + 'date' => [ + 'options' => [ + 'dateFormat' => $this->timezone->getDateFormatWithLongYear(), + ], + ], + ], + ]; + + $this->setData('config', $config); + + parent::prepare(); + } + /** * @inheritdoc */ diff --git a/app/code/Magento/Ui/TemplateEngine/Xhtml/Result.php b/app/code/Magento/Ui/TemplateEngine/Xhtml/Result.php index 066b4494e51d0..14283a8899e50 100644 --- a/app/code/Magento/Ui/TemplateEngine/Xhtml/Result.php +++ b/app/code/Magento/Ui/TemplateEngine/Xhtml/Result.php @@ -5,6 +5,8 @@ */ namespace Magento\Ui\TemplateEngine\Xhtml; +use Magento\Framework\App\ObjectManager; +use Magento\Framework\Serialize\Serializer\JsonHexTag; use Magento\Framework\View\Layout\Generator\Structure; use Magento\Framework\View\Element\UiComponentInterface; use Magento\Framework\View\TemplateEngine\Xhtml\Template; @@ -42,25 +44,33 @@ class Result implements ResultInterface */ protected $logger; + /** + * @var JsonHexTag + */ + private $jsonSerializer; + /** * @param Template $template * @param CompilerInterface $compiler * @param UiComponentInterface $component * @param Structure $structure * @param LoggerInterface $logger + * @param JsonHexTag $jsonSerializer */ public function __construct( Template $template, CompilerInterface $compiler, UiComponentInterface $component, Structure $structure, - LoggerInterface $logger + LoggerInterface $logger, + JsonHexTag $jsonSerializer = null ) { $this->template = $template; $this->compiler = $compiler; $this->component = $component; $this->structure = $structure; $this->logger = $logger; + $this->jsonSerializer = $jsonSerializer ?? ObjectManager::getInstance()->get(JsonHexTag::class); } /** @@ -81,7 +91,7 @@ public function getDocumentElement() public function appendLayoutConfiguration() { $layoutConfiguration = $this->wrapContent( - json_encode($this->structure->generate($this->component), JSON_HEX_TAG) + $this->jsonSerializer->serialize($this->structure->generate($this->component)) ); $this->template->append($layoutConfiguration); } diff --git a/app/code/Magento/Ui/Test/Mftf/ActionGroup/AdminDataGridFilterActionGroup.xml b/app/code/Magento/Ui/Test/Mftf/ActionGroup/AdminDataGridFilterActionGroup.xml index 8fbd5342b24c4..c1379b9fce575 100644 --- a/app/code/Magento/Ui/Test/Mftf/ActionGroup/AdminDataGridFilterActionGroup.xml +++ b/app/code/Magento/Ui/Test/Mftf/ActionGroup/AdminDataGridFilterActionGroup.xml @@ -30,7 +30,7 @@ <!--Clear all filters in grid--> <actionGroup name="clearFiltersAdminDataGrid"> <waitForPageLoad stepKey="waitForPageLoad"/> - <conditionalClick selector="{{AdminDataGridHeaderSection.clearFilters}}" dependentSelector="{{AdminDataGridHeaderSection.clearFilters}}" visible="true" stepKey="clearExistingOrderFilters"/> + <conditionalClick selector="{{AdminDataGridHeaderSection.clearFilters}}" dependentSelector="{{AdminDataGridHeaderSection.clearFilters}}" visible="true" stepKey="clearExistingFilters"/> </actionGroup> <actionGroup name="AdminGridFilterSearchResultsByInput"> diff --git a/app/code/Magento/Ui/Test/Mftf/Section/AdminDataGridTableSection.xml b/app/code/Magento/Ui/Test/Mftf/Section/AdminDataGridTableSection.xml index ea0f7e64a8448..28ee9efc9f0a2 100644 --- a/app/code/Magento/Ui/Test/Mftf/Section/AdminDataGridTableSection.xml +++ b/app/code/Magento/Ui/Test/Mftf/Section/AdminDataGridTableSection.xml @@ -7,7 +7,7 @@ --> <sections xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" - xsi:noNamespaceSchemaLocation="../../../../../../../dev/tests/acceptance/vendor/magento/magento2-functional-testing-framework/src/Magento/FunctionalTestingFramework/Page/etc/SectionObject.xsd"> + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Page/etc/SectionObject.xsd"> <section name="AdminDataGridTableSection"> <element name="firstRow" type="button" selector="tr.data-row:nth-of-type(1)"/> <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"/> @@ -18,5 +18,8 @@ <!--Specific cell e.g. {{Section.gridCell('1', 'Name')}}--> <element name="gridCell" type="text" selector="//tr[{{row}}]//td[count(//div[@data-role='grid-wrapper']//tr//th[contains(., '{{column}}')]/preceding-sibling::th) +1 ]" parameterized="true"/> <element name="rowViewAction" type="button" selector=".data-grid tbody > tr:nth-of-type({{row}}) .action-menu-item" parameterized="true" timeout="30"/> + <element name="rowActionSelect" type="button" selector="[data-role='grid'] tbody tr .action-select-wrap"/> + <element name="rowEditAction" type="button" selector="[data-role='grid'] tbody tr .action-select-wrap._active [data-action='item-edit']" timeout="30"/> + <element name="dataGridEmpty" type="block" selector=".data-grid-tr-no-data td"/> </section> </sections> diff --git a/app/code/Magento/Ui/Test/Mftf/Section/AdminMessagesSection.xml b/app/code/Magento/Ui/Test/Mftf/Section/AdminMessagesSection.xml index d3e94eb24dfd2..8880f7c3e1cc7 100644 --- a/app/code/Magento/Ui/Test/Mftf/Section/AdminMessagesSection.xml +++ b/app/code/Magento/Ui/Test/Mftf/Section/AdminMessagesSection.xml @@ -11,5 +11,6 @@ <section name="AdminMessagesSection"> <element name="successMessage" type="text" selector=".message-success"/> <element name="errorMessage" type="text" selector=".message.message-error.error"/> + <element name="warningMessage" type="text" selector=".message-warning"/> </section> </sections> diff --git a/app/code/Magento/Ui/Test/Mftf/Section/StorefrontMessagesSection.xml b/app/code/Magento/Ui/Test/Mftf/Section/StorefrontMessagesSection.xml new file mode 100644 index 0000000000000..b07f2d356b9ea --- /dev/null +++ b/app/code/Magento/Ui/Test/Mftf/Section/StorefrontMessagesSection.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="StorefrontMessagesSection"> + <element name="successMessage" type="text" selector=".message-success"/> + </section> +</sections> diff --git a/app/code/Magento/Ui/Test/Unit/Component/Listing/Columns/DateTest.php b/app/code/Magento/Ui/Test/Unit/Component/Listing/Columns/DateTest.php index 0bfb2a7dc0f34..414290efdd637 100644 --- a/app/code/Magento/Ui/Test/Unit/Component/Listing/Columns/DateTest.php +++ b/app/code/Magento/Ui/Test/Unit/Component/Listing/Columns/DateTest.php @@ -6,6 +6,7 @@ namespace Magento\Ui\Test\Unit\Component\Listing\Columns; use Magento\Framework\TestFramework\Unit\Helper\ObjectManager; +use PHPUnit_Framework_MockObject_MockObject as MockObject; /** * Class DateTest @@ -14,18 +15,33 @@ class DateTest extends \PHPUnit\Framework\TestCase { const TEST_TIME = '2000-04-12 16:34:12'; + private $data = [ + 'js_config' => [ + 'extends' => 'test_config_extends', + ], + 'config' => [ + 'dataType' => 'testType', + ], + 'name' => 'field_name', + ]; + /** - * @var \PHPUnit_Framework_MockObject_MockObject + * @var \Magento\Framework\View\Element\UiComponent\ContextInterface|MockObject */ protected $contextMock; + /** + * @var \Magento\Framework\View\Element\UiComponentFactory|MockObject + */ + private $uiComponentFactoryMock; + /** * @var \Magento\Ui\Component\Listing\Columns\Date */ protected $model; /** - * @var \PHPUnit_Framework_MockObject_MockObject + * @var \Magento\Framework\Stdlib\DateTime\TimezoneInterface|MockObject */ protected $timezoneMock; @@ -50,11 +66,7 @@ protected function setUp() true, [] ); - $processor = $this->getMockBuilder(\Magento\Framework\View\Element\UiComponent\Processor::class) - ->disableOriginalConstructor() - ->getMock(); - $this->contextMock->expects($this->never())->method('getProcessor')->willReturn($processor); - + $this->uiComponentFactoryMock = $this->createMock(\Magento\Framework\View\Element\UiComponentFactory::class); $this->timezoneMock = $this->getMockBuilder(\Magento\Framework\Stdlib\DateTime\TimezoneInterface::class) ->disableOriginalConstructor() ->getMock(); @@ -63,18 +75,63 @@ protected function setUp() \Magento\Ui\Component\Listing\Columns\Date::class, [ 'context' => $this->contextMock, - 'data' => [ - 'js_config' => [ - 'extends' => 'test_config_extends' - ], - 'config' => [ - 'dataType' => 'testType' + 'uiComponentFactory' => $this->uiComponentFactoryMock, + 'data' => $this->data, + 'timezone' => $this->timezoneMock, + ] + ); + } + + /** + * @return void + */ + public function testPrepare() + { + $dateFormat = 'M/d/Y'; + + $this->data['config']['filter'] = [ + 'filterType' => 'dateRange', + 'templates' => [ + 'date' => [ + 'options' => [ + 'dateFormat' => $dateFormat, ], - 'name' => 'field_name', ], - 'timezone' => $this->timezoneMock - ] + ], + ]; + + $this->timezoneMock->expects($this->once()) + ->method('getDateFormatWithLongYear') + ->willReturn($dateFormat); + + /** @var \Magento\Framework\View\Element\UiComponent\Processor|MockObject $processor */ + $processor = $this->getMockBuilder(\Magento\Framework\View\Element\UiComponent\Processor::class) + ->disableOriginalConstructor() + ->getMock(); + $this->contextMock->expects($this->atLeastOnce())->method('getProcessor')->willReturn($processor); + + /** @var \Magento\Framework\View\Element\UiComponentInterface|MockObject $wrappedComponentMock */ + $wrappedComponentMock = $this->getMockForAbstractClass( + \Magento\Framework\View\Element\UiComponentInterface::class, + [], + '', + false ); + + $wrappedComponentMock->expects($this->once()) + ->method('getContext') + ->willReturn($this->contextMock); + + $this->uiComponentFactoryMock->expects($this->once()) + ->method('create') + ->with( + $this->data['name'], + $this->data['config']['dataType'], + array_merge(['context' => $this->contextMock], $this->data) + ) + ->willReturn($wrappedComponentMock); + + $this->model->prepare(); } public function testPrepareDataSource() diff --git a/app/code/Magento/Ui/Test/Unit/TemplateEngine/Xhtml/ResultTest.php b/app/code/Magento/Ui/Test/Unit/TemplateEngine/Xhtml/ResultTest.php index 30a8e8005bbe8..9babfefd87f76 100644 --- a/app/code/Magento/Ui/Test/Unit/TemplateEngine/Xhtml/ResultTest.php +++ b/app/code/Magento/Ui/Test/Unit/TemplateEngine/Xhtml/ResultTest.php @@ -6,6 +6,7 @@ namespace Magento\Ui\Test\Unit\TemplateEngine\Xhtml; +use Magento\Framework\Serialize\Serializer\JsonHexTag; use Magento\Framework\TestFramework\Unit\Helper\ObjectManager; use Magento\Framework\View\Layout\Generator\Structure; use Magento\Framework\View\Element\UiComponentInterface; @@ -58,6 +59,11 @@ class ResultTest extends \PHPUnit\Framework\TestCase */ private $objectManager; + /** + * @var JsonHexTag|\PHPUnit_Framework_MockObject_MockObject + */ + private $serializer; + protected function setUp() { $this->template = $this->createPartialMock(Template::class, ['append']); @@ -65,6 +71,9 @@ protected function setUp() $this->component = $this->createMock(UiComponentInterface::class); $this->structure = $this->createPartialMock(Structure::class, ['generate']); $this->logger = $this->createMock(LoggerInterface::class); + $this->serializer = $this->getMockBuilder(JsonHexTag::class) + ->disableOriginalConstructor() + ->getMock(); $this->objectManager = new ObjectManager($this); $this->testModel = $this->objectManager->getObject(Result::class, [ @@ -73,6 +82,7 @@ protected function setUp() 'component' => $this->component, 'structure' => $this->structure, 'logger' => $this->logger, + 'jsonSerializer' => $this->serializer ]); } @@ -82,6 +92,10 @@ protected function setUp() public function testAppendLayoutConfiguration() { $configWithCdata = 'text before <![CDATA[cdata text]]>'; + $this->serializer->expects($this->once()) + ->method('serialize') + ->with([$configWithCdata]) + ->willReturn('["text before \u003C![CDATA[cdata text]]\u003E"]'); $this->structure->expects($this->once()) ->method('generate') ->with($this->component) diff --git a/app/code/Magento/Ui/i18n/en_US.csv b/app/code/Magento/Ui/i18n/en_US.csv index 981d444b31d53..040ec32cf0b74 100644 --- a/app/code/Magento/Ui/i18n/en_US.csv +++ b/app/code/Magento/Ui/i18n/en_US.csv @@ -202,3 +202,4 @@ CSV,CSV "Please enter at least {0} characters.","Please enter at least {0} characters." "Please enter a value between {0} and {1} characters long.","Please enter a value between {0} and {1} characters long." "Please enter a value between {0} and {1}.","Please enter a value between {0} and {1}." +"The file upload field is disabled.","The file upload field is disabled." diff --git a/app/code/Magento/Ui/view/base/web/js/form/element/file-uploader.js b/app/code/Magento/Ui/view/base/web/js/form/element/file-uploader.js index b81119f2bd5f3..4b178622c28cf 100644 --- a/app/code/Magento/Ui/view/base/web/js/form/element/file-uploader.js +++ b/app/code/Magento/Ui/view/base/web/js/form/element/file-uploader.js @@ -13,8 +13,9 @@ define([ 'Magento_Ui/js/modal/alert', 'Magento_Ui/js/lib/validation/validator', 'Magento_Ui/js/form/element/abstract', + 'mage/translate', 'jquery/file-uploader' -], function ($, _, utils, uiAlert, validator, Element) { +], function ($, _, utils, uiAlert, validator, Element, $t) { 'use strict'; return Element.extend({ @@ -328,6 +329,12 @@ define([ allowed = this.isFileAllowed(file), target = $(e.target); + if (this.disabled()) { + this.notifyError($t('The file upload field is disabled.')); + + return; + } + if (allowed.passed) { target.on('fileuploadsend', function (event, postData) { postData.data.append('param_name', this.paramName); diff --git a/app/code/Magento/Ui/view/base/web/js/form/element/wysiwyg.js b/app/code/Magento/Ui/view/base/web/js/form/element/wysiwyg.js index b8cd4a0f2c892..547e6cde59839 100644 --- a/app/code/Magento/Ui/view/base/web/js/form/element/wysiwyg.js +++ b/app/code/Magento/Ui/view/base/web/js/form/element/wysiwyg.js @@ -18,7 +18,7 @@ define([ return Abstract.extend({ defaults: { elementSelector: 'textarea', - value: '', + suffixRegExpPattern: '\\${ \\$.wysiwygUniqueSuffix }', $wysiwygEditorButton: '', links: { value: '${ $.provider }:${ $.dataScope }' @@ -52,6 +52,25 @@ define([ return this; }, + /** @inheritdoc */ + initConfig: function (config) { + var pattern = config.suffixRegExpPattern || this.constructor.defaults.suffixRegExpPattern; + + config.content = config.content.replace(new RegExp(pattern, 'g'), this.getUniqueSuffix(config)); + this._super(); + + return this; + }, + + /** + * Build unique id based on name, underscore separated. + * + * @param {Object} config + */ + getUniqueSuffix: function (config) { + return config.name.replace(/(\.|-)/g, '_'); + }, + /** * * @returns {exports} diff --git a/app/code/Magento/Ui/view/base/web/js/lib/knockout/bindings/bootstrap.js b/app/code/Magento/Ui/view/base/web/js/lib/knockout/bindings/bootstrap.js index b29d10a143117..a266e282eac66 100644 --- a/app/code/Magento/Ui/view/base/web/js/lib/knockout/bindings/bootstrap.js +++ b/app/code/Magento/Ui/view/base/web/js/lib/knockout/bindings/bootstrap.js @@ -26,7 +26,7 @@ define(function (require) { mageInit: require('./mage-init'), keyboard: require('./keyboard'), optgroup: require('./optgroup'), - aferRender: require('./after-render'), + afterRender: require('./after-render'), autoselect: require('./autoselect'), datepicker: require('./datepicker'), outerClick: require('./outer_click'), diff --git a/app/code/Magento/Ui/view/base/web/js/lib/knockout/extender/bound-nodes.js b/app/code/Magento/Ui/view/base/web/js/lib/knockout/extender/bound-nodes.js index a332b595bdf3c..6b3c437b90508 100644 --- a/app/code/Magento/Ui/view/base/web/js/lib/knockout/extender/bound-nodes.js +++ b/app/code/Magento/Ui/view/base/web/js/lib/knockout/extender/bound-nodes.js @@ -109,7 +109,7 @@ define([ wrapper.extend(ko, { /** - * Extends kncokouts' 'applyBindings' + * Extends knockouts' 'applyBindings' * to track nodes associated with model. * * @param {Function} orig - Original 'applyBindings' method. @@ -136,7 +136,7 @@ define([ }, /** - * Extends kncokouts' cleanNode + * Extends knockouts' cleanNode * to track nodes associated with model. * * @param {Function} orig - Original 'cleanNode' method. diff --git a/app/code/Magento/Ui/view/base/web/js/lib/validation/rules.js b/app/code/Magento/Ui/view/base/web/js/lib/validation/rules.js index 8e59f9d290906..cbfc0dae90dda 100644 --- a/app/code/Magento/Ui/view/base/web/js/lib/validation/rules.js +++ b/app/code/Magento/Ui/view/base/web/js/lib/validation/rules.js @@ -691,6 +691,24 @@ define([ }, $.mage.__('The value is not within the specified range.') ], + 'validate-positive-percent-decimal': [ + function (value) { + var numValue; + + if (utils.isEmptyNoTrim(value) || !/^\s*-?\d*(\.\d*)?\s*$/.test(value)) { + return false; + } + + numValue = utils.parseNumber(value); + + if (isNaN(numValue)) { + return false; + } + + return utils.isBetween(numValue, 0.01, 100); + }, + $.mage.__('Please enter a valid percentage discount value greater than 0.') + ], 'validate-digits': [ function (value) { return utils.isEmptyNoTrim(value) || !/[^\d]/.test(value); diff --git a/app/code/Magento/Ups/Model/Carrier.php b/app/code/Magento/Ups/Model/Carrier.php index b7a10e6f37944..a9e289d57300b 100644 --- a/app/code/Magento/Ups/Model/Carrier.php +++ b/app/code/Magento/Ups/Model/Carrier.php @@ -334,6 +334,14 @@ public function setRequest(RateRequest $request) $destCountry = self::GUAM_COUNTRY_ID; } + // For UPS, Las Palmas and Santa Cruz de Tenerife will be represented by Canary Islands country + if ( + $destCountry == self::SPAIN_COUNTRY_ID && + ($request->getDestRegionCode() == self::LAS_PALMAS_REGION_ID || $request->getDestRegionCode() == self::SANTA_CRUZ_DE_TENERIFE_REGION_ID) + ) { + $destCountry = self::CANARY_ISLANDS_COUNTRY_ID; + } + $country = $this->_countryFactory->create()->load($destCountry); $rowRequest->setDestCountry($country->getData('iso2_code') ?: $destCountry); diff --git a/app/code/Magento/UrlRewrite/Test/Mftf/Section/AdminUrlRewriteIndexSection.xml b/app/code/Magento/UrlRewrite/Test/Mftf/Section/AdminUrlRewriteIndexSection.xml index 455748a0da534..ab847f924b5cf 100644 --- a/app/code/Magento/UrlRewrite/Test/Mftf/Section/AdminUrlRewriteIndexSection.xml +++ b/app/code/Magento/UrlRewrite/Test/Mftf/Section/AdminUrlRewriteIndexSection.xml @@ -7,9 +7,10 @@ --> <sections xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" - xsi:noNamespaceSchemaLocation="../../../../../../../dev/tests/acceptance/vendor/magento/magento2-functional-testing-framework/src/Magento/FunctionalTestingFramework/Page/etc/SectionObject.xsd"> + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Page/etc/SectionObject.xsd"> <section name="AdminUrlRewriteIndexSection"> <element name="requestPathFilter" type="input" selector="#urlrewriteGrid_filter_request_path"/> <element name="requestPathColumnValue" type="text" selector="//*[@id='urlrewriteGrid']//tbody//td[@data-column='request_path' and normalize-space(.)='{{columnValue}}']" parameterized="true"/> + <element name="targetPathColumnValue" type="text" selector="//*[@id='urlrewriteGrid']//tbody//td[@data-column='target_path' and normalize-space(.)='{{columnValue}}']" parameterized="true"/> </section> </sections> diff --git a/app/code/Magento/UrlRewrite/Test/Mftf/Test/AdminCheckUrlRewritesCorrectlyGeneratedForMultipleStoreviewsDuringProductImportTest.xml b/app/code/Magento/UrlRewrite/Test/Mftf/Test/AdminCheckUrlRewritesCorrectlyGeneratedForMultipleStoreviewsDuringProductImportTest.xml new file mode 100644 index 0000000000000..cfe96fc1c33f7 --- /dev/null +++ b/app/code/Magento/UrlRewrite/Test/Mftf/Test/AdminCheckUrlRewritesCorrectlyGeneratedForMultipleStoreviewsDuringProductImportTest.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="AdminCheckUrlRewritesCorrectlyGeneratedForMultipleStoreviewsDuringProductImportTest"> + <annotations> + <features value="Url Rewrite"/> + <stories value="Url Rewrites for Multiple Storeviews"/> + <title value="Url Rewrites Correctly Generated for Multiple Storeviews During Product Import"/> + <description value="Check Url Rewrites Correctly Generated for Multiple Storeviews During Product Import."/> + <severity value="CRITICAL"/> + <testCaseId value="MAGETWO-76287"/> + <group value="urlRewrite"/> + </annotations> + <before> + <actionGroup ref="LoginAsAdmin" stepKey="loginAsAdmin"/> + <!-- Create Store View EN --> + <actionGroup ref="AdminCreateStoreViewActionGroup" stepKey="createStoreViewEn"> + <argument name="customStore" value="CustomStoreENNotUnique"/> + </actionGroup> + <!-- Create Store View NL --> + <actionGroup ref="AdminCreateStoreViewActionGroup" stepKey="createStoreViewNl"> + <argument name="customStore" value="CustomStoreNLNotUnique"/> + </actionGroup> + <createData entity="ApiCategory" stepKey="createCategory"> + <field key="name">category-admin</field> + </createData> + </before> + <after> + <deleteData createDataKey="createCategory" stepKey="deleteCategory"/> + <actionGroup ref="DeleteProductByName" stepKey="deleteImportedProduct"> + <argument name="product" value="productformagetwo76287"/> + </actionGroup> + <actionGroup ref="AdminDeleteStoreViewActionGroup" stepKey="deleteStoreViewEn"> + <argument name="customStore" value="CustomStoreENNotUnique"/> + </actionGroup> + <actionGroup ref="AdminDeleteStoreViewActionGroup" stepKey="deleteStoreViewNl"> + <argument name="customStore" value="CustomStoreNLNotUnique"/> + </actionGroup> + <actionGroup ref="logout" stepKey="logout"/> + </after> + <actionGroup ref="switchCategoryStoreView" stepKey="switchToStoreViewEn"> + <argument name="store" value="CustomStoreENNotUnique.name"/> + <argument name="catName" value="$$createCategory.name$$"/> + </actionGroup> + <uncheckOption selector="{{AdminCategoryBasicFieldSection.categoryNameUseDefault}}" stepKey="uncheckUseDefaultValueENStoreView"/> + <fillField selector="{{AdminCategoryBasicFieldSection.CategoryNameInput}}" userInput="category-english" stepKey="changeNameField"/> + <click selector="{{AdminCategorySEOSection.SectionHeader}}" stepKey="clickOnSectionHeader"/> + <actionGroup ref="ChangeSeoUrlKeyForSubCategory" stepKey="changeSeoUrlKeyENStoreView"> + <argument name="value" value="category-english"/> + </actionGroup> + <actionGroup ref="switchCategoryStoreView" stepKey="switchToStoreViewNl"> + <argument name="store" value="CustomStoreNLNotUnique.name"/> + <argument name="catName" value="$$createCategory.name$$"/> + </actionGroup> + <uncheckOption selector="{{AdminCategoryBasicFieldSection.categoryNameUseDefault}}" stepKey="uncheckUseDefaultValue1"/> + <fillField selector="{{AdminCategoryBasicFieldSection.CategoryNameInput}}" userInput="category-dutch" stepKey="changeNameFieldNLStoreView"/> + <click selector="{{AdminCategorySEOSection.SectionHeader}}" stepKey="clickOnSectionHeader2"/> + <actionGroup ref="ChangeSeoUrlKeyForSubCategory" stepKey="changeSeoUrlKeyNLStoreView"> + <argument name="value" value="category-dutch"/> + </actionGroup> + <amOnPage url="{{AdminImportIndexPage.url}}" stepKey="navigateToSystemImport"/> + <selectOption selector="{{AdminImportMainSection.entityType}}" userInput="Products" stepKey="selectProductsOption"/> + <waitForElementVisible selector="{{AdminImportMainSection.importBehavior}}" stepKey="waitForImportBehaviorElementVisible"/> + <selectOption selector="{{AdminImportMainSection.importBehavior}}" userInput="Add/Update" stepKey="selectAddUpdateOption"/> + <attachFile selector="{{AdminImportMainSection.selectFileToImport}}" userInput="import_updated.csv" stepKey="attachFileForImport"/> + <click selector="{{AdminImportHeaderSection.checkDataButton}}" stepKey="clickCheckDataButton"/> + <see selector="{{AdminMessagesSection.notice}}" userInput="Checked rows: 3, checked entities: 1, invalid rows: 0, total errors: 0" stepKey="assertNotice"/> + <see selector="{{AdminMessagesSection.successMessage}}" userInput="File is valid! To start import process press "Import" button" stepKey="assertSuccessMessage"/> + <click selector="{{AdminImportMainSection.importButton}}" stepKey="clickImportButton"/> + <see selector="{{AdminMessagesSection.successMessage}}" userInput="Import successfully done" stepKey="assertSuccessMessage1"/> + <actionGroup ref="SearchForProductOnBackendByNameActionGroup" stepKey="searchForProductOnBackend"> + <argument name="productName" value="productformagetwo76287"/> + </actionGroup> + <click selector="{{AdminProductGridSection.productRowBySku('productformagetwo76287')}}" stepKey="clickOnProductRow"/> + <grabFromCurrentUrl regex="~/id/(\d+)/~" stepKey="grabProductIdFromUrl"/> + <amOnPage url="{{AdminUrlRewriteIndexPage.url}}" stepKey="goToUrlRewritesIndexPage"/> + + <fillField selector="{{AdminUrlRewriteIndexSection.requestPathFilter}}" userInput="category-english.html" stepKey="inputCategoryUrlForENStoreView"/> + <click selector="{{AdminDataGridHeaderSection.applyFilters}}" stepKey="clickSearchButton"/> + <seeElement selector="{{AdminUrlRewriteIndexSection.requestPathColumnValue('category-english.html')}}" stepKey="seeUrlInRequestPathColumn"/> + <seeElement selector="{{AdminUrlRewriteIndexSection.targetPathColumnValue(catalog/category/view/id/$$createCategory.id$$)}}" stepKey="seeUrlInTargetPathColumn"/> + + <fillField selector="{{AdminUrlRewriteIndexSection.requestPathFilter}}" userInput="category-dutch.html" stepKey="inputCategoryUrlForNLStoreView"/> + <click selector="{{AdminDataGridHeaderSection.applyFilters}}" stepKey="clickSearchButton1"/> + <seeElement selector="{{AdminUrlRewriteIndexSection.requestPathColumnValue('category-dutch.html')}}" stepKey="seeUrlInRequestPathColumn1"/> + <seeElement selector="{{AdminUrlRewriteIndexSection.targetPathColumnValue(catalog/category/view/id/$$createCategory.id$$)}}" stepKey="seeUrlInTargetPathColumn1"/> + + <fillField selector="{{AdminUrlRewriteIndexSection.requestPathFilter}}" userInput="productformagetwo76287-english.html" stepKey="inputProductUrlForENStoreView"/> + <click selector="{{AdminDataGridHeaderSection.applyFilters}}" stepKey="clickSearchButton2"/> + <seeElement selector="{{AdminUrlRewriteIndexSection.requestPathColumnValue('productformagetwo76287-english.html')}}" stepKey="seeUrlInRequestPathColumn2"/> + <seeElement selector="{{AdminUrlRewriteIndexSection.targetPathColumnValue('catalog/product/view/id/$grabProductIdFromUrl')}}" stepKey="seeUrlInTargetPathColumn2"/> + + <fillField selector="{{AdminUrlRewriteIndexSection.requestPathFilter}}" userInput="productformagetwo76287-dutch.html" stepKey="inputProductUrlForENStoreView1"/> + <click selector="{{AdminDataGridHeaderSection.applyFilters}}" stepKey="clickSearchButton3"/> + <seeElement selector="{{AdminUrlRewriteIndexSection.requestPathColumnValue('productformagetwo76287-dutch.html')}}" stepKey="seeUrlInRequestPathColumn3"/> + <seeElement selector="{{AdminUrlRewriteIndexSection.targetPathColumnValue('catalog/product/view/id/$grabProductIdFromUrl')}}" stepKey="seeUrlInTargetPathColumn3"/> + + <fillField selector="{{AdminUrlRewriteIndexSection.requestPathFilter}}" userInput="category-english/productformagetwo76287-english.html" stepKey="inputProductUrlForENStoreView2"/> + <click selector="{{AdminDataGridHeaderSection.applyFilters}}" stepKey="clickSearchButton4"/> + <seeElement selector="{{AdminUrlRewriteIndexSection.requestPathColumnValue('category-english/productformagetwo76287-english.html')}}" stepKey="seeUrlInRequestPathColumn4"/> + <seeElement selector="{{AdminUrlRewriteIndexSection.targetPathColumnValue(catalog/product/view/id/$grabProductIdFromUrl/category/$$createCategory.id$$)}}" stepKey="seeUrlInTargetPathColumn4"/> + + <fillField selector="{{AdminUrlRewriteIndexSection.requestPathFilter}}" userInput="category-dutch/productformagetwo76287-dutch.html" stepKey="inputProductUrlForENStoreView3"/> + <click selector="{{AdminDataGridHeaderSection.applyFilters}}" stepKey="clickSearchButton5"/> + <seeElement selector="{{AdminUrlRewriteIndexSection.requestPathColumnValue('category-dutch/productformagetwo76287-dutch.html')}}" stepKey="seeUrlInRequestPathColumn5"/> + <seeElement selector="{{AdminUrlRewriteIndexSection.targetPathColumnValue(catalog/product/view/id/$grabProductIdFromUrl/category/$$createCategory.id$$)}}" stepKey="seeUrlInTargetPathColumn5"/> + </test> +</tests> diff --git a/app/code/Magento/User/Test/Mftf/Data/UserData.xml b/app/code/Magento/User/Test/Mftf/Data/UserData.xml index 751378da6c15d..fc8652442900b 100644 --- a/app/code/Magento/User/Test/Mftf/Data/UserData.xml +++ b/app/code/Magento/User/Test/Mftf/Data/UserData.xml @@ -7,7 +7,7 @@ --> <entities xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" - xsi:noNamespaceSchemaLocation="../../../../../../../dev/tests/acceptance/vendor/magento/magento2-functional-testing-framework/src/Magento/FunctionalTestingFramework/DataGenerator/etc/dataProfileSchema.xsd"> + xsi:noNamespaceSchemaLocation="urn:magento:mftf:DataGenerator/etc/dataProfileSchema.xsd"> <entity name="admin" type="user"> <data key="email">admin@magento.com</data> <data key="password">admin123</data> @@ -23,4 +23,18 @@ <data key="is_active">true</data> <data key="current_password">123123q</data> </entity> + <entity name="Admin3" type="user"> + <data key="username" unique="suffix">admin3</data> + <data key="firstname">admin3</data> + <data key="lastname">admin3</data> + <data key="email" unique="prefix">admin3WebUser@example.com</data> + <data key="password">123123q</data> + <data key="password_confirmation">123123q</data> + <data key="interface_local">en_US</data> + <data key="is_active">true</data> + <data key="current_password">123123q</data> + <array key="roles"> + <item>1</item> + </array> + </entity> </entities> diff --git a/app/code/Magento/User/Test/Mftf/Data/UserRoleData.xml b/app/code/Magento/User/Test/Mftf/Data/UserRoleData.xml index 64f64aeb5a1b2..104096df5813b 100644 --- a/app/code/Magento/User/Test/Mftf/Data/UserRoleData.xml +++ b/app/code/Magento/User/Test/Mftf/Data/UserRoleData.xml @@ -7,7 +7,7 @@ --> <entities xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" - xsi:noNamespaceSchemaLocation="../../../../../../../dev/tests/acceptance/vendor/magento/magento2-functional-testing-framework/src/Magento/FunctionalTestingFramework/DataGenerator/etc/dataProfileSchema.xsd"> + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/dataProfileSchema.xsd"> <entity name="adminProductInWebsiteRole" type="user_role"> <data key="rolename" unique="suffix">restrictedWebsiteRole</data> <data key="current_password">123123q</data> @@ -34,4 +34,16 @@ <data key="rolename" unique="suffix">RoleTest</data> <data key="current_password">123123q</data> </entity> + <entity name="adminMarketingInWebsiteRole" type="user_role"> + <data key="rolename" unique="suffix">restrictedWebsiteRole</data> + <data key="current_password">123123q</data> + <data key="gws_is_all">0</data> + <array key="gws_websites"> + <item>1</item> + </array> + <array key="resource"> + <item>Magento_Backend::marketing</item> + <item>Magento_SalesRule::quote</item> + </array> + </entity> </entities> diff --git a/app/code/Magento/User/Test/Mftf/Metadata/user-meta.xml b/app/code/Magento/User/Test/Mftf/Metadata/user-meta.xml index 9f8050983b215..1ee29be6b3d76 100644 --- a/app/code/Magento/User/Test/Mftf/Metadata/user-meta.xml +++ b/app/code/Magento/User/Test/Mftf/Metadata/user-meta.xml @@ -6,7 +6,7 @@ */ --> <operations xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" - xsi:noNamespaceSchemaLocation="../../../../../../vendor/magento/magento2-functional-testing-framework/src/Magento/FunctionalTestingFramework/DataGenerator/etc/dataOperation.xsd"> + xsi:noNamespaceSchemaLocation="urn:magento:mftf:DataGenerator/etc/dataOperation.xsd"> <operation name="CreateUser" dataType="user" type="create" auth="adminFormKey" url="/admin/user/save/" method="POST" successRegex="/messages-message-success/" returnRegex="" > <contentType>application/x-www-form-urlencoded</contentType> @@ -19,5 +19,8 @@ <field key="interface_locale">string</field> <field key="is_active">boolean</field> <field key="current_password">string</field> + <array key="roles"> + <value>string</value> + </array> </operation> </operations> diff --git a/app/code/Magento/Usps/Model/Carrier.php b/app/code/Magento/Usps/Model/Carrier.php index 1e8faf3cc9614..afe34cac575ac 100644 --- a/app/code/Magento/Usps/Model/Carrier.php +++ b/app/code/Magento/Usps/Model/Carrier.php @@ -1246,7 +1246,7 @@ protected function _getCountryName($countryId) 'FO' => 'Faroe Islands', 'FR' => 'France', 'GA' => 'Gabon', - 'GB' => 'Great Britain and Northern Ireland', + 'GB' => 'United Kingdom of Great Britain and Northern Ireland', 'GD' => 'Grenada', 'GE' => 'Georgia, Republic of', 'GF' => 'French Guiana', @@ -1364,7 +1364,7 @@ protected function _getCountryName($countryId) 'ST' => 'Sao Tome and Principe', 'SV' => 'El Salvador', 'SY' => 'Syrian Arab Republic', - 'SZ' => 'Swaziland', + 'SZ' => 'Eswatini', 'TC' => 'Turks and Caicos Islands', 'TD' => 'Chad', 'TG' => 'Togo', diff --git a/app/code/Magento/Widget/Test/Mftf/Section/AdminNewWidgetSection.xml b/app/code/Magento/Widget/Test/Mftf/Section/AdminNewWidgetSection.xml index b96d3865a6661..adf234baede72 100644 --- a/app/code/Magento/Widget/Test/Mftf/Section/AdminNewWidgetSection.xml +++ b/app/code/Magento/Widget/Test/Mftf/Section/AdminNewWidgetSection.xml @@ -27,5 +27,9 @@ <element name="selectAll" type="checkbox" selector=".admin__control-checkbox"/> <element name="sortById" type="button" selector="th.data-grid-th._sortable.not-sort.col-entity_id"/> <element name="sortByIdAscend" type="button" selector="th.data-grid-th._sortable._ascend.col-entity_id"/> + <element name="insertWidget" type="button" selector="#insert_button" timeout="30"/> + <element name="widgetTypeDropDown" type="select" selector="#select_widget_type"/> + <element name="conditionOperator" type="text" selector="//*[@id='conditions__1--1__attribute']/following-sibling::span[1]"/> + <element name="conditionOperatorSelect" type="select" selector="#conditions__1--{{arg1}}__operator" parameterized="true"/> </section> </sections> diff --git a/app/code/Magento/Widget/Test/Mftf/Section/StorefrontWidgetsSection.xml b/app/code/Magento/Widget/Test/Mftf/Section/StorefrontWidgetsSection.xml index 23908626389f9..9d1944cc126b8 100644 --- a/app/code/Magento/Widget/Test/Mftf/Section/StorefrontWidgetsSection.xml +++ b/app/code/Magento/Widget/Test/Mftf/Section/StorefrontWidgetsSection.xml @@ -11,5 +11,6 @@ <section name="StorefrontWidgetsSection"> <element name="widgetProductsGrid" type="block" selector=".block.widget.block-products-list.grid"/> <element name="widgetProductName" type="text" selector=".product-item-name"/> + <element name="checkElementStorefrontByPrice" type="text" selector="//*[@class='product-items widget-product-grid']//*[contains(text(),'${{arg2}}.00')]" parameterized="true"/> </section> </sections> diff --git a/app/code/Magento/Wishlist/Block/Customer/Wishlist/Item/Column/Cart.php b/app/code/Magento/Wishlist/Block/Customer/Wishlist/Item/Column/Cart.php index b043a8d4b684c..1ba31b26df46e 100644 --- a/app/code/Magento/Wishlist/Block/Customer/Wishlist/Item/Column/Cart.php +++ b/app/code/Magento/Wishlist/Block/Customer/Wishlist/Item/Column/Cart.php @@ -6,6 +6,8 @@ namespace Magento\Wishlist\Block\Customer\Wishlist\Item\Column; +use Magento\Catalog\Controller\Adminhtml\Product\Initialization\StockDataFilter; + /** * Wishlist block customer item cart column * @@ -35,4 +37,28 @@ public function getProductItem() { return $this->getItem()->getProduct(); } + + /** + * Get min and max qty for wishlist form. + * + * @return array + */ + public function getMinMaxQty(): array + { + $stockItem = $this->stockRegistry->getStockItem( + $this->getItem()->getProduct()->getId(), + $this->getItem()->getProduct()->getStore()->getWebsiteId() + ); + + $params = []; + + $params['minAllowed'] = (float)$stockItem->getMinSaleQty(); + if ($stockItem->getMaxSaleQty()) { + $params['maxAllowed'] = (float)$stockItem->getMaxSaleQty(); + } else { + $params['maxAllowed'] = (float)StockDataFilter::MAX_QTY_VALUE; + } + + return $params; + } } diff --git a/app/code/Magento/Wishlist/Test/Mftf/ActionGroup/StorefrontCustomerWishlistActionGroup.xml b/app/code/Magento/Wishlist/Test/Mftf/ActionGroup/StorefrontCustomerWishlistActionGroup.xml index 87a77526e0af5..920b387441ada 100644 --- a/app/code/Magento/Wishlist/Test/Mftf/ActionGroup/StorefrontCustomerWishlistActionGroup.xml +++ b/app/code/Magento/Wishlist/Test/Mftf/ActionGroup/StorefrontCustomerWishlistActionGroup.xml @@ -87,4 +87,26 @@ <click selector="{{StorefrontCustomerWishlistProductSection.productUpdateWishList}}" stepKey="submitUpdateWishlist"/> <see selector="{{StorefrontMessagesSection.success}}" userInput="{{product.name}} has been updated in your Wish List." stepKey="successMessage"/> </actionGroup> + + <actionGroup name="StorefrontCustomerEditProductInWishlistMakeQuantityValidationError"> + <arguments> + <argument name="product"/> + <argument name="description" type="string"/> + <argument name="quantity" type="string"/> + <argument name="errorNum" type="string"/> + </arguments> + <scrollToTopOfPage stepKey="scrollToTop1"/> + <moveMouseOver selector="{{StorefrontCustomerWishlistProductSection.productInfoByName(product.name)}}" stepKey="mouseOverOnProduct" /> + <fillField selector="{{StorefrontCustomerWishlistProductSection.productDescription(product.name)}}" userInput="{{description}}" stepKey="fillDescription"/> + <fillField selector="{{StorefrontCustomerWishlistProductSection.productQuantity(product.name)}}" userInput="{{quantity}}" stepKey="fillQuantity"/> + <moveMouseOver selector="{{StorefrontCustomerWishlistProductSection.productAddAllToCart}}" stepKey="mouseOver1"/> + <click selector="{{StorefrontCustomerWishlistProductSection.productUpdateWishList}}" stepKey="clickAddToWishlistButton"/> + <waitForElement selector="{{StorefrontCustomerWishlistProductSection.productQtyError(product.name)}}" stepKey="waitForErrorMessage"/> + <scrollToTopOfPage stepKey="scrollToTop2"/> + + <!--Check error message--> + <moveMouseOver selector="{{StorefrontCustomerWishlistProductSection.productInfoByName(product.name)}}" stepKey="wishlistMoveMouseOverProduct" /> + <see selector="{{StorefrontCustomerWishlistProductSection.productQtyError(product.name)}}" userInput="The maximum you may purchase is {{errorNum}}." stepKey="checkQtyError"/> + <moveMouseOver selector="{{StorefrontCustomerWishlistProductSection.productAddAllToCart}}" stepKey="mouseOver2"/> + </actionGroup> </actionGroups> diff --git a/app/code/Magento/Wishlist/Test/Mftf/Section/StorefrontCustomerWishlistProductSection.xml b/app/code/Magento/Wishlist/Test/Mftf/Section/StorefrontCustomerWishlistProductSection.xml index 88d68a460da74..ea9c4748f67b1 100644 --- a/app/code/Magento/Wishlist/Test/Mftf/Section/StorefrontCustomerWishlistProductSection.xml +++ b/app/code/Magento/Wishlist/Test/Mftf/Section/StorefrontCustomerWishlistProductSection.xml @@ -17,7 +17,9 @@ <element name="productImageByImageName" type="text" selector="//main//li//a//img[contains(@src, '{{var1}}')]" parameterized="true"/> <element name="productDescription" type="input" selector="//a[contains(text(), '{{productName}}')]/ancestor::div[@class='product-item-info']//textarea[@class='product-item-comment']" parameterized="true"/> <element name="productQuantity" type="input" selector="//a[contains(text(), '{{productName}}')]/ancestor::div[@class='product-item-info']//input[@class='input-text qty']" parameterized="true"/> + <element name="productEditButtonByName" type="button" selector="//li[.//a[contains(text(), '{{var1}}')]]//span[contains(text(), 'Edit')]" parameterized="true"/> <element name="productUpdateWishList" type="button" selector=".column.main .actions-toolbar .action.update" timeout="30"/> <element name="productAddAllToCart" type="button" selector=".column.main .actions-toolbar .action.tocart" timeout="30"/> + <element name="productQtyError" type="text" selector="//li[.//a[contains(text(), '{{var1}}')]]//div[@class='mage-error']" parameterized="true" timeout="30"/> </section> </sections> diff --git a/app/code/Magento/Wishlist/Test/Mftf/Test/StorefrontAddMultipleStoreProductsToWishlistTest.xml b/app/code/Magento/Wishlist/Test/Mftf/Test/StorefrontAddMultipleStoreProductsToWishlistTest.xml index 60745d15e7688..b9ea513288c81 100644 --- a/app/code/Magento/Wishlist/Test/Mftf/Test/StorefrontAddMultipleStoreProductsToWishlistTest.xml +++ b/app/code/Magento/Wishlist/Test/Mftf/Test/StorefrontAddMultipleStoreProductsToWishlistTest.xml @@ -12,7 +12,6 @@ <title value="Add products to wishlist from different stores"/> <description value="All products added to wishlist should be visible on any store. Even if product visibility was set to 'Not Visible Individually' for this store"/> <group value="wishlist"/> - <group value="skip" /><!-- Skipped; see MAGETWO-94100 --> </annotations> <before> <getData entity="DefaultRootCategoryGetter" stepKey="getDefaultRootCategory"/> diff --git a/app/code/Magento/Wishlist/Test/Mftf/Test/StorefrontCheckAmountLimitWishlistTest.xml b/app/code/Magento/Wishlist/Test/Mftf/Test/StorefrontCheckAmountLimitWishlistTest.xml new file mode 100644 index 0000000000000..3bc924a7e60fa --- /dev/null +++ b/app/code/Magento/Wishlist/Test/Mftf/Test/StorefrontCheckAmountLimitWishlistTest.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="StorefrontCheckAmountLimitWishlistTest"> + <annotations> + <stories value="MAGETWO-73613: My Wishlist - quantity input box issue"/> + <title value="Check amount limit for Wishlist"/> + <description value="Check amount limit for Wishlist with different config settings"/> + <features value="Wishlist"/> + <severity value="AVERAGE"/> + <testCaseId value="MAGETWO-96606"/> + <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="CustomerLogoutStorefrontActionGroup" stepKey="logoutCustomerAccount"/> + <deleteData createDataKey="createCustomer" stepKey="deleteCustomer"/> + <createData entity="DefaultProductStockOptions" stepKey="changeToDefaultQtyAllowAmount"/> + </after> + <!--Login as Customer--> + <actionGroup ref="CustomerLoginOnStorefront" stepKey="loginToStorefrontAccount"> + <argument name="customer" value="$$createCustomer$$"/> + </actionGroup> + <!--Go to category page--> + <amOnPage url="{{StorefrontCategoryPage.url($$createCategory.name$$)}}" stepKey="goToCreateCategoryPage"/> + <!--Add created product to Wish List--> + <actionGroup ref="StorefrontCustomerAddCategoryProductToWishlistActionGroup" stepKey="addSimpleProduct1ToWishlist"> + <argument name="productVar" value="$$createProduct$$"/> + </actionGroup> + <actionGroup ref="StorefrontCustomerEditProductInWishlistMakeQuantityValidationError" stepKey="checkWishListError1"> + <argument name="product" value="$$createProduct$$"/> + <argument name="description" value="It`s my dream"/> + <argument name="quantity" value="1234567890"/> + <argument name="errorNum" value="10000"/> + </actionGroup> + <createData entity="ProductStockOptions" stepKey="changeDefaultQtyAllowAmount"/> + <!--Go to wishlist page--> + <amOnPage url="{{StorefrontCustomerWishlistPage.url}}" stepKey="amOnWishlist" /> + <actionGroup ref="StorefrontCustomerEditProductInWishlistMakeQuantityValidationError" stepKey="checkWishListError2"> + <argument name="product" value="$$createProduct$$"/> + <argument name="description" value="It`s my dream"/> + <argument name="quantity" value="1234567890"/> + <argument name="errorNum" value="99999999"/> + </actionGroup> + </test> +</tests> diff --git a/app/code/Magento/Wishlist/view/frontend/templates/item/column/cart.phtml b/app/code/Magento/Wishlist/view/frontend/templates/item/column/cart.phtml index 9ea0d1a823235..49c35331b7868 100644 --- a/app/code/Magento/Wishlist/view/frontend/templates/item/column/cart.phtml +++ b/app/code/Magento/Wishlist/view/frontend/templates/item/column/cart.phtml @@ -11,6 +11,7 @@ /** @var \Magento\Wishlist\Model\Item $item */ $item = $block->getItem(); $product = $item->getProduct(); +$allowedQty = $block->getMinMaxQty(); ?> <?php foreach ($block->getChildNames() as $childName): ?> <?= /* @noEscape */ $block->getLayout()->renderElement($childName, false) ?> @@ -21,7 +22,7 @@ $product = $item->getProduct(); <div class="field qty"> <label class="label" for="qty[<?= $block->escapeHtmlAttr($item->getId()) ?>]"><span><?= $block->escapeHtml(__('Qty')) ?></span></label> <div class="control"> - <input type="number" data-role="qty" id="qty[<?= $block->escapeHtmlAttr($item->getId()) ?>]" class="input-text qty" data-validate="{'required-number':true,'validate-greater-than-zero':true}" + <input type="number" data-role="qty" id="qty[<?= $block->escapeHtmlAttr($item->getId()) ?>]" class="input-text qty" data-validate="{'required-number':true,'validate-greater-than-zero':true, 'validate-item-quantity':{'minAllowed':<?= /* @noEscape */ $allowedQty['minAllowed'] ?>,'maxAllowed':<?= /* @noEscape */ $allowedQty['maxAllowed'] ?>}}" name="qty[<?= $block->escapeHtmlAttr($item->getId()) ?>]" value="<?= /* @noEscape */ (int)($block->getAddToCartQty($item) * 1) ?>"> </div> </div> diff --git a/app/code/Magento/Wishlist/view/frontend/web/js/add-to-wishlist.js b/app/code/Magento/Wishlist/view/frontend/web/js/add-to-wishlist.js index 7ce934317263b..cab130f7c2104 100644 --- a/app/code/Magento/Wishlist/view/frontend/web/js/add-to-wishlist.js +++ b/app/code/Magento/Wishlist/view/frontend/web/js/add-to-wishlist.js @@ -63,12 +63,6 @@ define([ isFileUploaded = false, self = this; - if (event.handleObj.selector == this.options.qtyInfo) { //eslint-disable-line eqeqeq - this._updateAddToWishlistButton({}); - event.stopPropagation(); - - return; - } $(event.handleObj.selector).each(function (index, element) { if ($(element).is('input[type=text]') || $(element).is('input[type=email]') || @@ -89,9 +83,7 @@ define([ } }); - if (isFileUploaded) { - this.bindFormSubmit(); - } + this.bindFormSubmit(isFileUploaded); this._updateAddToWishlistButton(dataToAdd); event.stopPropagation(); }, @@ -195,34 +187,45 @@ define([ /** * Bind form submit. + * + * @param {Boolean} isFileUploaded */ - bindFormSubmit: function () { + bindFormSubmit: function (isFileUploaded) { var self = this; $('[data-action="add-to-wishlist"]').on('click', function (event) { var element, params, form, action; - event.stopPropagation(); - event.preventDefault(); + if (!$($(self.options.qtyInfo).closest('form')).valid()) { + event.stopPropagation(); + event.preventDefault(); - element = $('input[type=file]' + self.options.customOptionsInfo); - params = $(event.currentTarget).data('post'); - form = $(element).closest('form'); - action = params.action; - - if (params.data.id) { - $('<input>', { - type: 'hidden', - name: 'id', - value: params.data.id - }).appendTo(form); + return; } - if (params.data.uenc) { - action += 'uenc/' + params.data.uenc; - } + if (isFileUploaded) { - $(form).attr('action', action).submit(); + element = $('input[type=file]' + self.options.customOptionsInfo); + params = $(event.currentTarget).data('post'); + form = $(element).closest('form'); + action = params.action; + + if (params.data.id) { + $('<input>', { + type: 'hidden', + name: 'id', + value: params.data.id + }).appendTo(form); + } + + if (params.data.uenc) { + action += 'uenc/' + params.data.uenc; + } + + $(form).attr('action', action).submit(); + event.stopPropagation(); + event.preventDefault(); + } }); } }); diff --git a/app/design/adminhtml/Magento/backend/Magento_Backend/web/css/source/module/_menu.less b/app/design/adminhtml/Magento/backend/Magento_Backend/web/css/source/module/_menu.less index a3142b56abd27..42c9b289afdb7 100644 --- a/app/design/adminhtml/Magento/backend/Magento_Backend/web/css/source/module/_menu.less +++ b/app/design/adminhtml/Magento/backend/Magento_Backend/web/css/source/module/_menu.less @@ -15,9 +15,9 @@ @menu__background-color: @color-very-dark-grayish-orange; -@menu-logo__padding-bottom: 2.2rem; +@menu-logo__padding-bottom: 1.7rem; @menu-logo__outer-size: @menu-logo__padding-top + @menu-logo-img__height + @menu-logo__padding-bottom; -@menu-logo__padding-top: 1.2rem; +@menu-logo__padding-top: 1.7rem; @menu-logo-img__height: 4.1rem; @menu-logo-img__width: 3.5rem; diff --git a/app/design/adminhtml/Magento/backend/Magento_Backend/web/css/source/module/main/_collapsible-blocks.less b/app/design/adminhtml/Magento/backend/Magento_Backend/web/css/source/module/main/_collapsible-blocks.less index 6420738c6fb9b..ed607102a1084 100644 --- a/app/design/adminhtml/Magento/backend/Magento_Backend/web/css/source/module/main/_collapsible-blocks.less +++ b/app/design/adminhtml/Magento/backend/Magento_Backend/web/css/source/module/main/_collapsible-blocks.less @@ -127,8 +127,24 @@ } } } + td.admin__collapsible-block-wrapper { + .admin__collapsible-title { + &:before { + content: @icon-expand-open__content; + } + } + &._show { + .admin__collapsible-title { + &:before { + content: @icon-expand-close__content; + } + } + } + } } + + &.fieldset-wrapper { border-bottom: 1px solid @collapsible__border-color; padding: 0; diff --git a/app/design/adminhtml/Magento/backend/Magento_Backend/web/css/source/module/main/actions-bar/_store-switcher.less b/app/design/adminhtml/Magento/backend/Magento_Backend/web/css/source/module/main/actions-bar/_store-switcher.less index 80bebb22a9043..0bd6cc62bd509 100644 --- a/app/design/adminhtml/Magento/backend/Magento_Backend/web/css/source/module/main/actions-bar/_store-switcher.less +++ b/app/design/adminhtml/Magento/backend/Magento_Backend/web/css/source/module/main/actions-bar/_store-switcher.less @@ -10,11 +10,6 @@ // ToDo UI: Consist old styles, should be changed with new design .store-switcher { - color: @text__color; // ToDo UI: Delete with admin scope - float: left; - font-size: round(@font-size__base - .1rem, 1); - margin-top: .7rem; - .admin__action-dropdown { background-color: @page-main-actions__background-color; margin-left: .5em; @@ -47,8 +42,8 @@ width: 7px; } &::-webkit-scrollbar-thumb { - border-radius: 4px; background-color: rgba(0, 0, 0, .5); + border-radius: 4px; } li { @@ -116,6 +111,12 @@ } } } + + color: @text__color; // ToDo UI: Delete with admin scope + float: left; + font-size: round(@font-size__base - .1rem, 1); + margin-top: .59rem; + } .store-switcher-label { @@ -239,7 +240,6 @@ .store-switcher-label { display: inline-block; - margin-top: @indent__s; } } diff --git a/app/design/adminhtml/Magento/backend/Magento_Catalog/web/css/source/_module.less b/app/design/adminhtml/Magento/backend/Magento_Catalog/web/css/source/_module.less index 3355950254072..ffbbaeb084162 100644 --- a/app/design/adminhtml/Magento/backend/Magento_Catalog/web/css/source/_module.less +++ b/app/design/adminhtml/Magento/backend/Magento_Catalog/web/css/source/_module.less @@ -15,6 +15,14 @@ } } +.catalog-category-edit { + .admin__grid-control { + .admin__grid-control-value { + display: none; + } + } +} + .product-composite-configure-inner { .admin__control-text { &.qty { diff --git a/app/design/adminhtml/Magento/backend/Magento_Review/web/css/source/_module.less b/app/design/adminhtml/Magento/backend/Magento_Review/web/css/source/_module.less index 17be2ca706076..faa5845405ab2 100644 --- a/app/design/adminhtml/Magento/backend/Magento_Review/web/css/source/_module.less +++ b/app/design/adminhtml/Magento/backend/Magento_Review/web/css/source/_module.less @@ -34,7 +34,7 @@ .admin__field-control { direction: rtl; display: inline-block; - margin: -4px 0 0; + margin: -2px 0 0; unicode-bidi: bidi-override; vertical-align: top; width: 125px; diff --git a/app/design/adminhtml/Magento/backend/Magento_Sales/web/css/source/module/_order.less b/app/design/adminhtml/Magento/backend/Magento_Sales/web/css/source/module/_order.less index 1e76679f594c1..fa1ae25628986 100644 --- a/app/design/adminhtml/Magento/backend/Magento_Sales/web/css/source/module/_order.less +++ b/app/design/adminhtml/Magento/backend/Magento_Sales/web/css/source/module/_order.less @@ -92,6 +92,14 @@ margin: 0; padding: 0; } + .admin__data-grid-pager-wrap{ + .selectmenu { + margin-bottom: 10px; + } + } + .data-grid-search-control-wrap { + margin-bottom: 10px; + } } // diff --git a/app/design/adminhtml/Magento/backend/etc/view.xml b/app/design/adminhtml/Magento/backend/etc/view.xml index f10f7789b0888..18c2d8f1b1722 100644 --- a/app/design/adminhtml/Magento/backend/etc/view.xml +++ b/app/design/adminhtml/Magento/backend/etc/view.xml @@ -23,6 +23,8 @@ </images> </media> <exclude> + <item type="file">Lib::mage/captcha.js</item> + <item type="file">Lib::mage/captcha.min.js</item> <item type="file">Lib::mage/common.js</item> <item type="file">Lib::mage/cookies.js</item> <item type="file">Lib::mage/dataPost.js</item> diff --git a/app/design/adminhtml/Magento/backend/web/css/source/components/_messages.less b/app/design/adminhtml/Magento/backend/web/css/source/components/_messages.less index de24bf89620d4..8773de61185e2 100644 --- a/app/design/adminhtml/Magento/backend/web/css/source/components/_messages.less +++ b/app/design/adminhtml/Magento/backend/web/css/source/components/_messages.less @@ -76,7 +76,7 @@ position: absolute; speak: none; text-shadow: none; - top: 1.3rem; + top: 1.5rem; width: auto; } } @@ -110,7 +110,7 @@ content: @alert-icon__error__content; font-size: @alert-icon__error__font-size; left: 2.2rem; - margin-top: 0.5rem; + margin-top: .5rem; } } diff --git a/app/design/adminhtml/Magento/backend/web/css/source/components/_modals_extend.less b/app/design/adminhtml/Magento/backend/web/css/source/components/_modals_extend.less index 63d97dc52e453..565b127ccad3e 100644 --- a/app/design/adminhtml/Magento/backend/web/css/source/components/_modals_extend.less +++ b/app/design/adminhtml/Magento/backend/web/css/source/components/_modals_extend.less @@ -146,13 +146,13 @@ } .action-close { - padding: @modal-popup__padding; + padding: @modal-popup__padding - 2; &:active, &:focus { background: transparent; - padding-right: @modal-popup__padding + (@modal-action-close__font-size - @modal-action-close__active__font-size) / 2; - padding-top: @modal-popup__padding + (@modal-action-close__font-size - @modal-action-close__active__font-size) / 2; + padding-right: @modal-popup__padding - 2; + padding-top: @modal-popup__padding - 2; } } } diff --git a/app/design/adminhtml/Magento/backend/web/css/styles-old.less b/app/design/adminhtml/Magento/backend/web/css/styles-old.less index 6d46145df388c..116163cc375a8 100644 --- a/app/design/adminhtml/Magento/backend/web/css/styles-old.less +++ b/app/design/adminhtml/Magento/backend/web/css/styles-old.less @@ -2738,7 +2738,8 @@ // --------------------------------------------- #widget_instace_tabs_properties_section_content .widget-option-label { - margin-top: 6px; + margin-top: 7px; + display: inline-block; } // @@ -4832,6 +4833,9 @@ margin-bottom: @indent__m; margin-top: -@indent__xl; } + select + .mage-error { + margin-top: 0; + } } } } 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 2dd8463308a2c..85ef048ef0fc7 100644 --- a/app/design/frontend/Magento/blank/Magento_Catalog/web/css/source/_module.less +++ b/app/design/frontend/Magento/blank/Magento_Catalog/web/css/source/_module.less @@ -488,6 +488,7 @@ .product-items-names { .product-item { + display: flex; margin-bottom: @indent__s; } diff --git a/app/design/frontend/Magento/blank/Magento_Catalog/web/css/source/module/_listings.less b/app/design/frontend/Magento/blank/Magento_Catalog/web/css/source/module/_listings.less index 8b727b0d9c28d..951ca89a07988 100644 --- a/app/design/frontend/Magento/blank/Magento_Catalog/web/css/source/module/_listings.less +++ b/app/design/frontend/Magento/blank/Magento_Catalog/web/css/source/module/_listings.less @@ -63,7 +63,6 @@ } &-actions { - display: none; .actions-secondary { > button.action { diff --git a/app/design/frontend/Magento/blank/Magento_Customer/web/css/source/_module.less b/app/design/frontend/Magento/blank/Magento_Customer/web/css/source/_module.less index 3ffaeb82cdc2a..7da0433108703 100644 --- a/app/design/frontend/Magento/blank/Magento_Customer/web/css/source/_module.less +++ b/app/design/frontend/Magento/blank/Magento_Customer/web/css/source/_module.less @@ -421,7 +421,7 @@ > .field { > .control { - width: 55%; + width: 80%; } } } diff --git a/app/design/frontend/Magento/blank/Magento_Wishlist/web/css/source/_module.less b/app/design/frontend/Magento/blank/Magento_Wishlist/web/css/source/_module.less index 0e8350261e002..f34ed404e86d9 100644 --- a/app/design/frontend/Magento/blank/Magento_Wishlist/web/css/source/_module.less +++ b/app/design/frontend/Magento/blank/Magento_Wishlist/web/css/source/_module.less @@ -194,6 +194,7 @@ &-actions { display: block; + float: left; .action { margin-right: 15px; diff --git a/app/design/frontend/Magento/blank/web/css/source/_forms.less b/app/design/frontend/Magento/blank/web/css/source/_forms.less index 94b993b53b508..c9f3c3d72ef4c 100644 --- a/app/design/frontend/Magento/blank/web/css/source/_forms.less +++ b/app/design/frontend/Magento/blank/web/css/source/_forms.less @@ -18,7 +18,7 @@ .fieldset { .lib-form-fieldset(); &:last-child { - margin-bottom: 0; + margin-bottom: @indent__base; } > .field, 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 228c6947c938b..6aa0e50f3c393 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 @@ -563,6 +563,7 @@ .product-items-names { .product-item { + display: flex; margin-bottom: @indent__s; } diff --git a/app/design/frontend/Magento/luma/Magento_Catalog/web/css/source/module/_listings.less b/app/design/frontend/Magento/luma/Magento_Catalog/web/css/source/module/_listings.less index d0382d34d39fc..6bf766b7400a7 100644 --- a/app/design/frontend/Magento/luma/Magento_Catalog/web/css/source/module/_listings.less +++ b/app/design/frontend/Magento/luma/Magento_Catalog/web/css/source/module/_listings.less @@ -68,7 +68,6 @@ } &-actions { - display: none; .actions-secondary { > button.action { diff --git a/app/design/frontend/Magento/luma/Magento_Checkout/web/css/source/module/_minicart.less b/app/design/frontend/Magento/luma/Magento_Checkout/web/css/source/module/_minicart.less index 547460c387c33..7090da10b46c0 100644 --- a/app/design/frontend/Magento/luma/Magento_Checkout/web/css/source/module/_minicart.less +++ b/app/design/frontend/Magento/luma/Magento_Checkout/web/css/source/module/_minicart.less @@ -110,9 +110,9 @@ @_toggle-selector: ~'.action.showcart', @_options-selector: ~'.block-minicart', @_dropdown-list-width: 320px, - @_dropdown-list-position-right: 0px, + @_dropdown-list-position-right: -10px, @_dropdown-list-pointer-position: right, - @_dropdown-list-pointer-position-left-right: 26px, + @_dropdown-list-pointer-position-left-right: 12px, @_dropdown-list-z-index: 101, @_dropdown-toggle-icon-content: @icon-cart, @_dropdown-toggle-active-icon-content: @icon-cart, @@ -412,7 +412,6 @@ margin-left: 13px; .block-minicart { - right: -15px; width: 390px; } } diff --git a/app/design/frontend/Magento/luma/Magento_Checkout/web/css/source/module/checkout/_checkout.less b/app/design/frontend/Magento/luma/Magento_Checkout/web/css/source/module/checkout/_checkout.less index 0df0cace338c0..3ea1f5b7f6842 100644 --- a/app/design/frontend/Magento/luma/Magento_Checkout/web/css/source/module/checkout/_checkout.less +++ b/app/design/frontend/Magento/luma/Magento_Checkout/web/css/source/module/checkout/_checkout.less @@ -48,6 +48,7 @@ .step-title { &:extend(.abs-checkout-title all); .lib-css(border-bottom, @checkout-step-title__border); + margin-bottom: 15px; } .step-content { diff --git a/app/design/frontend/Magento/luma/Magento_Checkout/web/css/source/module/checkout/_order-summary.less b/app/design/frontend/Magento/luma/Magento_Checkout/web/css/source/module/checkout/_order-summary.less index 5ecc4d4713bf1..9bad9518f5724 100644 --- a/app/design/frontend/Magento/luma/Magento_Checkout/web/css/source/module/checkout/_order-summary.less +++ b/app/design/frontend/Magento/luma/Magento_Checkout/web/css/source/module/checkout/_order-summary.less @@ -137,6 +137,10 @@ } .product-item { + .product-item-details { + &:extend(.abs-add-clearfix all); + } + .product-item-inner { display: table; margin: 0 0 @indent__s; @@ -166,6 +170,10 @@ } } } + + .message { + margin-top: 10px; + } } .actions-toolbar { diff --git a/app/design/frontend/Magento/luma/Magento_Customer/web/css/source/_module.less b/app/design/frontend/Magento/luma/Magento_Customer/web/css/source/_module.less index d7ae6c3b28f4a..ade56d7f929c6 100755 --- a/app/design/frontend/Magento/luma/Magento_Customer/web/css/source/_module.less +++ b/app/design/frontend/Magento/luma/Magento_Customer/web/css/source/_module.less @@ -371,7 +371,7 @@ .fieldset { > .field { > .control { - width: 55%; + width: 80%; } } } diff --git a/app/design/frontend/Magento/luma/Magento_Multishipping/web/css/source/_module.less b/app/design/frontend/Magento/luma/Magento_Multishipping/web/css/source/_module.less index 7662c60734a1b..9761f36b96344 100644 --- a/app/design/frontend/Magento/luma/Magento_Multishipping/web/css/source/_module.less +++ b/app/design/frontend/Magento/luma/Magento_Multishipping/web/css/source/_module.less @@ -374,7 +374,7 @@ text-align: right; .action { - margin-left: @indent__s; + margin-left: 0; &.back { display: block; @@ -496,4 +496,12 @@ margin-left: @indent__xl; } } + + .multicheckout { + .actions-toolbar { + > .primary { + margin-right: 0; + } + } + } } diff --git a/app/design/frontend/Magento/luma/Magento_Theme/web/css/source/_module.less b/app/design/frontend/Magento/luma/Magento_Theme/web/css/source/_module.less index 68938ed206038..f3e0f810481ed 100644 --- a/app/design/frontend/Magento/luma/Magento_Theme/web/css/source/_module.less +++ b/app/design/frontend/Magento/luma/Magento_Theme/web/css/source/_module.less @@ -151,6 +151,12 @@ } } + .page-print { + .nav-toggle { + display: none; + } + } + .page-main { > .page-title-wrapper { .page-title + .action { @@ -319,6 +325,23 @@ } } } + .page-header { + .switcher { + .options { + ul.dropdown { + right: 0; + &:before { + left: auto; + right: 10px; + } + &:after { + left: auto; + right: 9px; + } + } + } + } + } // // Widgets diff --git a/app/design/frontend/Magento/luma/Magento_Wishlist/web/css/source/_module.less b/app/design/frontend/Magento/luma/Magento_Wishlist/web/css/source/_module.less index 584eefb9bc643..3b6f724508220 100644 --- a/app/design/frontend/Magento/luma/Magento_Wishlist/web/css/source/_module.less +++ b/app/design/frontend/Magento/luma/Magento_Wishlist/web/css/source/_module.less @@ -203,6 +203,7 @@ &-actions { display: block; + float: left; .action { margin-right: 15px; diff --git a/app/design/frontend/Magento/luma/web/css/source/_forms.less b/app/design/frontend/Magento/luma/web/css/source/_forms.less index 7c5027aef113b..6701d5f9e9d21 100644 --- a/app/design/frontend/Magento/luma/web/css/source/_forms.less +++ b/app/design/frontend/Magento/luma/web/css/source/_forms.less @@ -20,7 +20,7 @@ .lib-form-fieldset(); &:last-child { - margin-bottom: 0; + margin-bottom: @indent__base; } > .field, diff --git a/app/design/frontend/Magento/luma/web/css/source/components/_modals_extend.less b/app/design/frontend/Magento/luma/web/css/source/components/_modals_extend.less index ed01ef7d027f5..0e34d7b87387d 100644 --- a/app/design/frontend/Magento/luma/web/css/source/components/_modals_extend.less +++ b/app/design/frontend/Magento/luma/web/css/source/components/_modals_extend.less @@ -83,7 +83,8 @@ .modal-slide { .action-close { - padding: @modal-slide-action-close__padding; + margin: 15px; + padding: 0; } .page-main-actions { diff --git a/app/etc/di.xml b/app/etc/di.xml index ad77ae3adc566..05fd34a178ded 100755 --- a/app/etc/di.xml +++ b/app/etc/di.xml @@ -1356,4 +1356,11 @@ <argument name="scopeType" xsi:type="const">Magento\Framework\App\Config\ScopeConfigInterface::SCOPE_TYPE_DEFAULT</argument> </arguments> </type> + <type name="Magento\Framework\App\ScopeResolverPool"> + <arguments> + <argument name="scopeResolvers" xsi:type="array"> + <item name="default" xsi:type="object">Magento\Framework\App\ScopeResolver</item> + </argument> + </arguments> + </type> </config> diff --git a/composer.json b/composer.json index 34341f7c3230a..d04bf269d4485 100644 --- a/composer.json +++ b/composer.json @@ -74,7 +74,7 @@ "ramsey/uuid": "~3.7.3" }, "require-dev": { - "magento/magento2-functional-testing-framework": "2.3.11", + "magento/magento2-functional-testing-framework": "2.3.13", "phpunit/phpunit": "~6.2.0", "squizlabs/php_codesniffer": "3.2.2", "phpmd/phpmd": "@stable", diff --git a/composer.lock b/composer.lock index ad8cb4ba740cc..384ff14618a80 100644 --- a/composer.lock +++ b/composer.lock @@ -1,10 +1,10 @@ { "_readme": [ "This file locks the dependencies of your project to a known state", - "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#composer-lock-the-lock-file", + "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "2fd90a780b9d54a8f73f0554a4b5df98", + "content-hash": "d57f148fd874b8685bf73bbf0a0ea75a", "packages": [ { "name": "braintree/braintree_php", @@ -1756,7 +1756,7 @@ }, { "name": "Gert de Pagter", - "email": "BackEndTea@gmail.com" + "email": "backendtea@gmail.com" } ], "description": "Symfony polyfill for ctype functions", @@ -4058,16 +4058,16 @@ "packages-dev": [ { "name": "allure-framework/allure-codeception", - "version": "1.2.7", + "version": "1.3.0", "source": { "type": "git", "url": "https://github.com/allure-framework/allure-codeception.git", - "reference": "48598f4b4603b50b663bfe977260113a40912131" + "reference": "9d31d781b3622b028f1f6210bc76ba88438bd518" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/allure-framework/allure-codeception/zipball/48598f4b4603b50b663bfe977260113a40912131", - "reference": "48598f4b4603b50b663bfe977260113a40912131", + "url": "https://api.github.com/repos/allure-framework/allure-codeception/zipball/9d31d781b3622b028f1f6210bc76ba88438bd518", + "reference": "9d31d781b3622b028f1f6210bc76ba88438bd518", "shasum": "" }, "require": { @@ -4105,7 +4105,7 @@ "steps", "testing" ], - "time": "2018-03-07T11:18:27+00:00" + "time": "2018-12-18T19:47:23+00:00" }, { "name": "allure-framework/allure-php-api", @@ -5972,23 +5972,24 @@ }, { "name": "magento/magento2-functional-testing-framework", - "version": "2.3.11", + "version": "2.3.13", "source": { "type": "git", "url": "https://github.com/magento/magento2-functional-testing-framework.git", - "reference": "3ca1bd74228a61bd05520bed1ef88b5a19764d92" + "reference": "2c8a4c3557c9a8412eb2ea50ce3f69abc2f47ba1" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/magento/magento2-functional-testing-framework/zipball/3ca1bd74228a61bd05520bed1ef88b5a19764d92", - "reference": "3ca1bd74228a61bd05520bed1ef88b5a19764d92", + "url": "https://api.github.com/repos/magento/magento2-functional-testing-framework/zipball/2c8a4c3557c9a8412eb2ea50ce3f69abc2f47ba1", + "reference": "2c8a4c3557c9a8412eb2ea50ce3f69abc2f47ba1", "shasum": "" }, "require": { - "allure-framework/allure-codeception": "~1.2.6", - "codeception/codeception": "~2.3.4", + "allure-framework/allure-codeception": "~1.3.0", + "codeception/codeception": "~2.3.4 || ~2.4.0 ", "consolidation/robo": "^1.0.0", "epfremme/swagger-php": "^2.0", + "ext-curl": "*", "flow/jsonpath": ">0.2", "fzaninotto/faker": "^1.6", "monolog/monolog": "^1.0", @@ -6005,6 +6006,7 @@ "goaop/framework": "2.2.0", "php-coveralls/php-coveralls": "^1.0", "phpmd/phpmd": "^2.6.0", + "phpunit/phpunit": "~6.5.0 || ~7.0.0", "rregeer/phpunit-coverage-check": "^0.1.4", "sebastian/phpcpd": "~3.0 || ~4.0", "squizlabs/php_codesniffer": "~3.2", @@ -6039,7 +6041,7 @@ "magento", "testing" ], - "time": "2018-11-13T18:22:25+00:00" + "time": "2019-01-29T15:31:14+00:00" }, { "name": "moontoast/math", diff --git a/dev/tests/acceptance/tests/_data/catalog_products.csv b/dev/tests/acceptance/tests/_data/catalog_products.csv new file mode 100644 index 0000000000000..e6651ffcfae21 --- /dev/null +++ b/dev/tests/acceptance/tests/_data/catalog_products.csv @@ -0,0 +1,4 @@ +sku,store_view_code,attribute_set_code,product_type,categories,product_websites,name,description,short_description,weight,product_online,tax_class_name,visibility,price,special_price,special_price_from_date,special_price_to_date,url_key,meta_title,meta_keywords,meta_description,base_image,base_image_label,small_image,small_image_label,thumbnail_image,thumbnail_image_label,swatch_image,swatch_image_label,created_at,updated_at,new_from_date,new_to_date,display_product_options_in,map_price,msrp_price,map_enabled,gift_message_available,custom_design,custom_design_from,custom_design_to,custom_layout_update,page_layout,product_options_container,msrp_display_actual_price_type,country_of_manufacture,additional_attributes,qty,out_of_stock_qty,use_config_min_qty,is_qty_decimal,allow_backorders,use_config_backorders,min_cart_qty,use_config_min_sale_qty,max_cart_qty,use_config_max_sale_qty,is_in_stock,notify_on_stock_below,use_config_notify_stock_qty,manage_stock,use_config_manage_stock,use_config_qty_increments,qty_increments,use_config_enable_qty_inc,enable_qty_increments,is_decimal_divided,website_id,deferred_stock_update,use_config_deferred_stock_update,related_skus,related_position,crosssell_skus,crosssell_position,upsell_skus,upsell_position,additional_images,additional_image_labels,hide_from_product_page,custom_options,bundle_price_type,bundle_sku_type,bundle_price_view,bundle_weight_type,bundle_values,bundle_shipment_type,configurable_variations,configurable_variation_labels,associated_skus +"Simple Product for Test",,Default,simple,,base,"Simple Product for Test",,,,1,"Taxable Goods","Catalog, Search",123.0000,,,,simple-product-for-test,,,,,,,,,,,,"12/26/18, 5:05 AM","12/26/18, 5:05 AM",,,"Block after Info Column",,,,,,,,,,,"Use config",,,1000.0000,0.0000,1,0,0,1,1.0000,1,0.0000,1,1,,1,0,1,1,0.0000,1,0,0,0,0,1,,,,,,,,,,,,,,,,,,, +"Virtual Product for Test",,Default,virtual,,base,"Virtual Product for Test",,,0.0000,1,"Taxable Goods","Catalog, Search",99.9900,,,,virtual-product-for-test,,,,,,,,,,,,"12/26/18, 5:05 AM","12/26/18, 5:05 AM",,,"Block after Info Column",,,,,,,,,,,"Use config",,,1000.0000,0.0000,1,0,0,1,1.0000,1,0.0000,1,1,,1,0,1,1,0.0000,1,0,0,0,0,1,,,,,,,,,,,,,,,,,,, +"Api Downloadable Product for Test",,Default,downloadable,,base,"Api Downloadable Product for Test","API Product Description5c23605cc09b71","API Product Short Description5c23605cc0a111",,1,"Taxable Goods","Catalog, Search",123.0000,,,,api-downloadable-product-for-test,,,,,,,,,,,,"12/26/18, 5:05 AM","12/26/18, 5:05 AM",,,"Block after Info Column",,,,,,,,,,,"Use config",,,1000.0000,0.0000,1,0,0,1,1.0000,1,0.0000,1,1,,1,0,1,1,0.0000,1,0,0,0,0,1,,,,,,,,,,,,,,,,,,, diff --git a/dev/tests/acceptance/tests/_data/import_updated.csv b/dev/tests/acceptance/tests/_data/import_updated.csv new file mode 100644 index 0000000000000..620af02641ecc --- /dev/null +++ b/dev/tests/acceptance/tests/_data/import_updated.csv @@ -0,0 +1,4 @@ +product_websites,store_view_code,attribute_set_code,product_type,categories,sku,price,name,url_key +base,,Default,simple,Default Category/category-admin,productformagetwo76287,123,productformagetwo76287,productformagetwo76287 +,en,Default,simple,,productformagetwo76287,,productformagetwo76287-english,productformagetwo76287-english +,nl,Default,simple,,productformagetwo76287,,productformagetwo76287-dutch,productformagetwo76287-dutch diff --git a/dev/tests/acceptance/tests/_suite/WYSIWYGDisabledSuite.xml b/dev/tests/acceptance/tests/_suite/WYSIWYGDisabledSuite.xml new file mode 100644 index 0000000000000..65c2bb7004503 --- /dev/null +++ b/dev/tests/acceptance/tests/_suite/WYSIWYGDisabledSuite.xml @@ -0,0 +1,23 @@ +<?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="../../../../../vendor/magento/magento2-functional-testing-framework/src/Magento/FunctionalTestingFramework/Suite/etc/suiteSchema.xsd"> + <suite name="WYSIWYGDisabledSuite"> + <before> + <magentoCLI stepKey="disableWYSYWYG" command="config:set cms/wysiwyg/enabled disabled" /> + </before> + <include> + <group name="WYSIWYGDisabled"/> + </include> + <exclude> + <group name="skip"/> + </exclude> + <after> + <magentoCLI stepKey="disableWYSYWYG" command="config:set cms/wysiwyg/enabled enabled" /> + </after> + </suite> +</suites> \ No newline at end of file diff --git a/dev/tests/api-functional/testsuite/Magento/Customer/Api/CustomerMetadataTest.php b/dev/tests/api-functional/testsuite/Magento/Customer/Api/CustomerMetadataTest.php index f2632aa1481e4..6a68925f76cbc 100644 --- a/dev/tests/api-functional/testsuite/Magento/Customer/Api/CustomerMetadataTest.php +++ b/dev/tests/api-functional/testsuite/Magento/Customer/Api/CustomerMetadataTest.php @@ -9,6 +9,7 @@ use Magento\Customer\Api\Data\CustomerInterface as Customer; use Magento\Customer\Model\Data\AttributeMetadata; use Magento\TestFramework\TestCase\WebapiAbstract; +use Magento\TestFramework\Helper\Bootstrap; /** * Class CustomerMetadataTest @@ -19,6 +20,19 @@ class CustomerMetadataTest extends WebapiAbstract const SERVICE_VERSION = "V1"; const RESOURCE_PATH = "/V1/attributeMetadata/customer"; + /** + * @var CustomerMetadataInterface + */ + private $customerMetadata; + + /** + * @inheritdoc + */ + public function setUp() + { + $this->customerMetadata = Bootstrap::getObjectManager()->create(CustomerMetadataInterface::class); + } + /** * Test retrieval of attribute metadata for the customer entity type. * @@ -200,8 +214,7 @@ public function testGetCustomAttributesMetadata() $attributeMetadata = $this->_webApiCall($serviceInfo); - // There are no default custom attributes. - $this->assertCount(0, $attributeMetadata); + $this->assertCount(count($this->customerMetadata->getCustomAttributesMetadata()), $attributeMetadata); } /** diff --git a/dev/tests/api-functional/testsuite/Magento/Sales/Service/V1/CreditMemoCreateRefundTest.php b/dev/tests/api-functional/testsuite/Magento/Sales/Service/V1/CreditMemoCreateRefundTest.php index eae0e600434a6..2b9c539f64e66 100644 --- a/dev/tests/api-functional/testsuite/Magento/Sales/Service/V1/CreditMemoCreateRefundTest.php +++ b/dev/tests/api-functional/testsuite/Magento/Sales/Service/V1/CreditMemoCreateRefundTest.php @@ -10,7 +10,7 @@ use Magento\TestFramework\TestCase\WebapiAbstract; /** - * Class CreditMemoCreateRefundTest + * API tests for CreditMemoCreateRefund. */ class CreditMemoCreateRefundTest extends WebapiAbstract { @@ -25,12 +25,17 @@ class CreditMemoCreateRefundTest extends WebapiAbstract */ protected $objectManager; + /** + * @inheritdoc + */ protected function setUp() { $this->objectManager = \Magento\TestFramework\Helper\Bootstrap::getObjectManager(); } /** + * Test for Invoke method. + * * @magentoApiDataFixture Magento/Sales/_files/invoice.php */ public function testInvoke() @@ -115,8 +120,7 @@ public function testInvoke() ); $this->assertNotEmpty($result); $order = $this->objectManager->get(OrderRepositoryInterface::class)->get($order->getId()); - //Totally refunded orders still can be processed and shipped. - $this->assertEquals(Order::STATE_PROCESSING, $order->getState()); + $this->assertEquals(Order::STATE_CLOSED, $order->getState()); } private function getItemsForRest($order) 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 6715c73510290..0eb9e4229b957 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 @@ -197,4 +197,42 @@ private function getBundleProduct(array $items): array return []; } + + /** + * @return void + * @magentoApiDataFixture Magento/Sales/_files/order_with_tax.php + */ + public function testOrderGetExtensionAttributes() + { + $expectedTax = [ + 'code' => 'US-NY-*-Rate 1', + 'type' => 'shipping', + ]; + + /** @var \Magento\Sales\Model\Order $order */ + $order = $this->objectManager->create(\Magento\Sales\Model\Order::class); + $order->loadByIncrementId(self::ORDER_INCREMENT_ID); + + $serviceInfo = [ + 'rest' => [ + 'resourcePath' => self::RESOURCE_PATH . '/' . $order->getId(), + 'httpMethod' => \Magento\Framework\Webapi\Rest\Request::HTTP_METHOD_GET, + ], + 'soap' => [ + 'service' => self::SERVICE_READ_NAME, + 'serviceVersion' => self::SERVICE_VERSION, + 'operation' => self::SERVICE_READ_NAME . 'get', + ], + ]; + $result = $this->_webApiCall($serviceInfo, ['id' => $order->getId()]); + + $appliedTaxes = $result['extension_attributes']['applied_taxes']; + $this->assertEquals($expectedTax['code'], $appliedTaxes[0]['code']); + $appliedTaxes = $result['extension_attributes']['item_applied_taxes']; + $this->assertEquals($expectedTax['type'], $appliedTaxes[0]['type']); + $this->assertNotEmpty($appliedTaxes[0]['applied_taxes']); + $this->assertEquals(true, $result['extension_attributes']['converting_from_quote']); + $this->assertArrayHasKey('payment_additional_info', $result['extension_attributes']); + $this->assertNotEmpty($result['extension_attributes']['payment_additional_info']); + } } diff --git a/dev/tests/api-functional/testsuite/Magento/Sales/Service/V1/OrderItemGetTest.php b/dev/tests/api-functional/testsuite/Magento/Sales/Service/V1/OrderItemGetTest.php index 3ab93f9aecb99..a527c69c7e92d 100644 --- a/dev/tests/api-functional/testsuite/Magento/Sales/Service/V1/OrderItemGetTest.php +++ b/dev/tests/api-functional/testsuite/Magento/Sales/Service/V1/OrderItemGetTest.php @@ -77,4 +77,37 @@ protected function assertOrderItem(\Magento\Sales\Model\Order\Item $orderItem, a $this->assertEquals($orderItem->getBasePrice(), $response['base_price']); $this->assertEquals($orderItem->getRowTotal(), $response['row_total']); } + + /** + * @return void + * @magentoApiDataFixture Magento/Sales/_files/order_with_discount.php + */ + public function testGetOrderWithDiscount() + { + /** @var \Magento\Sales\Model\Order $order */ + $order = $this->objectManager->create(\Magento\Sales\Model\Order::class); + $order->loadByIncrementId(self::ORDER_INCREMENT_ID); + /** @var \Magento\Sales\Model\Order\Item $orderItem */ + $orderItem = current($order->getItems()); + + $serviceInfo = [ + 'rest' => [ + 'resourcePath' => self::RESOURCE_PATH . '/' . $orderItem->getId(), + 'httpMethod' => \Magento\Framework\Webapi\Rest\Request::HTTP_METHOD_GET, + ], + 'soap' => [ + 'service' => self::SERVICE_NAME, + 'serviceVersion' => self::SERVICE_VERSION, + 'operation' => self::SERVICE_NAME . 'get', + ], + ]; + + $response = $this->_webApiCall($serviceInfo, ['id' => $orderItem->getId()]); + + $this->assertTrue(is_array($response)); + $this->assertEquals(8.00, $response['row_total']); + $this->assertEquals(8.00, $response['base_row_total']); + $this->assertEquals(9.00, $response['row_total_incl_tax']); + $this->assertEquals(9.00, $response['base_row_total_incl_tax']); + } } diff --git a/dev/tests/api-functional/testsuite/Magento/Sales/Service/V1/OrderListTest.php b/dev/tests/api-functional/testsuite/Magento/Sales/Service/V1/OrderListTest.php index 0500f31858291..5e092bb1ebd69 100644 --- a/dev/tests/api-functional/testsuite/Magento/Sales/Service/V1/OrderListTest.php +++ b/dev/tests/api-functional/testsuite/Magento/Sales/Service/V1/OrderListTest.php @@ -30,9 +30,78 @@ protected function setUp() } /** + * @return void * @magentoApiDataFixture Magento/Sales/_files/order_list.php */ public function testOrderList() + { + $searchData = $this->getSearchData(); + + $requestData = ['searchCriteria' => $searchData]; + $serviceInfo = [ + 'rest' => [ + 'resourcePath' => self::RESOURCE_PATH . '?' . http_build_query($requestData), + 'httpMethod' => \Magento\Framework\Webapi\Rest\Request::HTTP_METHOD_GET, + ], + 'soap' => [ + 'service' => self::SERVICE_READ_NAME, + 'serviceVersion' => self::SERVICE_VERSION, + 'operation' => self::SERVICE_READ_NAME . 'getList', + ], + ]; + + $result = $this->_webApiCall($serviceInfo, $requestData); + $this->assertArrayHasKey('items', $result); + $this->assertCount(2, $result['items']); + $this->assertArrayHasKey('search_criteria', $result); + $this->assertEquals($searchData, $result['search_criteria']); + $this->assertEquals('100000002', $result['items'][0]['increment_id']); + $this->assertEquals('100000001', $result['items'][1]['increment_id']); + } + + /** + * @return void + * @magentoApiDataFixture Magento/Sales/_files/order_list_with_tax.php + */ + public function testOrderListExtensionAttributes() + { + $searchData = $this->getSearchData(); + + $requestData = ['searchCriteria' => $searchData]; + $serviceInfo = [ + 'rest' => [ + 'resourcePath' => self::RESOURCE_PATH . '?' . http_build_query($requestData), + 'httpMethod' => \Magento\Framework\Webapi\Rest\Request::HTTP_METHOD_GET, + ], + 'soap' => [ + 'service' => self::SERVICE_READ_NAME, + 'serviceVersion' => self::SERVICE_VERSION, + 'operation' => self::SERVICE_READ_NAME . 'getList', + ], + ]; + + $result = $this->_webApiCall($serviceInfo, $requestData); + + $expectedTax = [ + 'code' => 'US-NY-*-Rate 1', + 'type' => 'shipping', + ]; + $appliedTaxes = $result['items'][0]['extension_attributes']['applied_taxes']; + $this->assertEquals($expectedTax['code'], $appliedTaxes[0]['code']); + $appliedTaxes = $result['items'][0]['extension_attributes']['item_applied_taxes']; + $this->assertEquals($expectedTax['type'], $appliedTaxes[0]['type']); + $this->assertNotEmpty($appliedTaxes[0]['applied_taxes']); + $this->assertEquals(true, $result['items'][0]['extension_attributes']['converting_from_quote']); + $this->assertArrayHasKey('payment_additional_info', $result['items'][0]['extension_attributes']); + $this->assertNotEmpty($result['items'][0]['extension_attributes']['payment_additional_info']); + } + + /** + * Get search data for request. + * + * @return array + */ + private function getSearchData() : array { /** @var \Magento\Framework\Api\SortOrderBuilder $sortOrderBuilder */ $sortOrderBuilder = $this->objectManager->get( @@ -70,25 +139,6 @@ public function testOrderList() $searchCriteriaBuilder->addSortOrder($sortOrder); $searchData = $searchCriteriaBuilder->create()->__toArray(); - $requestData = ['searchCriteria' => $searchData]; - $serviceInfo = [ - 'rest' => [ - 'resourcePath' => self::RESOURCE_PATH . '?' . http_build_query($requestData), - 'httpMethod' => \Magento\Framework\Webapi\Rest\Request::HTTP_METHOD_GET, - ], - 'soap' => [ - 'service' => self::SERVICE_READ_NAME, - 'serviceVersion' => self::SERVICE_VERSION, - 'operation' => self::SERVICE_READ_NAME . 'getList', - ], - ]; - - $result = $this->_webApiCall($serviceInfo, $requestData); - $this->assertArrayHasKey('items', $result); - $this->assertCount(2, $result['items']); - $this->assertArrayHasKey('search_criteria', $result); - $this->assertEquals($searchData, $result['search_criteria']); - $this->assertEquals('100000002', $result['items'][0]['increment_id']); - $this->assertEquals('100000001', $result['items'][1]['increment_id']); + return $searchData; } } diff --git a/dev/tests/api-functional/testsuite/Magento/Sales/Service/V1/RefundOrderTest.php b/dev/tests/api-functional/testsuite/Magento/Sales/Service/V1/RefundOrderTest.php index aacda763ca2aa..69bbecc1317a7 100644 --- a/dev/tests/api-functional/testsuite/Magento/Sales/Service/V1/RefundOrderTest.php +++ b/dev/tests/api-functional/testsuite/Magento/Sales/Service/V1/RefundOrderTest.php @@ -5,8 +5,10 @@ */ namespace Magento\Sales\Service\V1; +use Magento\Sales\Model\Order; + /** - * API test for creation of Creditmemo for certain Order. + * API tests for creation of Creditmemo for certain Order. */ class RefundOrderTest extends \Magento\TestFramework\TestCase\WebapiAbstract { @@ -23,6 +25,9 @@ class RefundOrderTest extends \Magento\TestFramework\TestCase\WebapiAbstract */ private $creditmemoRepository; + /** + * @inheritdoc + */ protected function setUp() { $this->objectManager = \Magento\TestFramework\Helper\Bootstrap::getObjectManager(); @@ -86,9 +91,8 @@ public function testShortRequest() 'Failed asserting that proper shipping amount of the Order was refunded' ); - //Totally refunded orders can be processed. $this->assertEquals( - $existingOrder->getStatus(), + Order::STATE_COMPLETE, $updatedOrder->getStatus(), 'Failed asserting that order status has not changed' ); diff --git a/dev/tests/functional/tests/app/Magento/Analytics/Test/TestCase/NavigateMenuTest.xml b/dev/tests/functional/tests/app/Magento/Analytics/Test/TestCase/NavigateMenuTest.xml index 9c19f80e91d39..4a1ccf3bcb044 100644 --- a/dev/tests/functional/tests/app/Magento/Analytics/Test/TestCase/NavigateMenuTest.xml +++ b/dev/tests/functional/tests/app/Magento/Analytics/Test/TestCase/NavigateMenuTest.xml @@ -10,7 +10,7 @@ <variation name="NavigateMenuTestBIEssentials" summary="Navigate through BI Essentials admin menu to Sign Up page" ticketId="MAGETWO-63700"> <data name="menuItem" xsi:type="string">Reports > BI Essentials</data> <data name="waitMenuItemNotVisible" xsi:type="boolean">false</data> - <data name="businessIntelligenceLink" xsi:type="string">https://dashboard.rjmetrics.com/v2/magento/signup</data> + <data name="businessIntelligenceLink" xsi:type="string">https://account.magento.com/onboarding/steps/view/step/gmv/</data> <constraint name="Magento\Analytics\Test\Constraint\AssertBIEssentialsLink" /> </variation> <variation name="NavigateMenuTestAdvancedReporting" summary="Navigate through Advanced Reporting admin menu to BI Reports page" ticketId="MAGETWO-65748"> diff --git a/dev/tests/functional/tests/app/Magento/Catalog/Test/Repository/CatalogProductSimple.xml b/dev/tests/functional/tests/app/Magento/Catalog/Test/Repository/CatalogProductSimple.xml index 721b0ff570079..e90ca6bf7868a 100644 --- a/dev/tests/functional/tests/app/Magento/Catalog/Test/Repository/CatalogProductSimple.xml +++ b/dev/tests/functional/tests/app/Magento/Catalog/Test/Repository/CatalogProductSimple.xml @@ -41,7 +41,7 @@ <field name="attribute_set_id" xsi:type="array"> <item name="dataset" xsi:type="string">default</item> </field> - <field name="name" xsi:type="string">Product \'!@#$%^&*()+:;\\|}{][?=-~` %isolation%</field> + <field name="name" xsi:type="string">Product \'!@#$%^&*()+:;\\|}{][?=~` %isolation%</field> <field name="sku" xsi:type="string">sku_simple_product_%isolation%</field> <field name="is_virtual" xsi:type="string">No</field> <field name="product_has_weight" xsi:type="string">This item has weight</field> diff --git a/dev/tests/functional/tests/app/Magento/Catalog/Test/TestCase/Product/ProductTypeSwitchingOnCreationTest.xml b/dev/tests/functional/tests/app/Magento/Catalog/Test/TestCase/Product/ProductTypeSwitchingOnCreationTest.xml index 7c4824c604e29..49cd2a347c687 100644 --- a/dev/tests/functional/tests/app/Magento/Catalog/Test/TestCase/Product/ProductTypeSwitchingOnCreationTest.xml +++ b/dev/tests/functional/tests/app/Magento/Catalog/Test/TestCase/Product/ProductTypeSwitchingOnCreationTest.xml @@ -58,7 +58,6 @@ <constraint name="Magento\ConfigurableProduct\Test\Constraint\AssertChildProductIsNotDisplayedSeparately" /> </variation> <variation name="ProductTypeSwitchingOnCreationTestVariation7"> - <data name="tag" xsi:type="string">stable:no</data> <data name="createProduct" xsi:type="string">virtual</data> <data name="product" xsi:type="string">downloadableProduct::default</data> <constraint name="Magento\Catalog\Test\Constraint\AssertProductSaveMessage" /> diff --git a/dev/tests/functional/tests/app/Magento/Catalog/Test/TestCase/Product/ProductTypeSwitchingOnUpdateTest.php b/dev/tests/functional/tests/app/Magento/Catalog/Test/TestCase/Product/ProductTypeSwitchingOnUpdateTest.php index 43741393e7968..90cd6bdb76328 100644 --- a/dev/tests/functional/tests/app/Magento/Catalog/Test/TestCase/Product/ProductTypeSwitchingOnUpdateTest.php +++ b/dev/tests/functional/tests/app/Magento/Catalog/Test/TestCase/Product/ProductTypeSwitchingOnUpdateTest.php @@ -143,5 +143,6 @@ protected function clearDownloadableData() /** @var Downloadable $downloadableInfoTab */ $downloadableInfoTab = $this->catalogProductEdit->getProductForm()->getSection('downloadable_information'); $downloadableInfoTab->getDownloadableBlock('Links')->clearDownloadableData(); + $downloadableInfoTab->setIsDownloadable('No'); } } diff --git a/dev/tests/functional/tests/app/Magento/Catalog/Test/TestCase/Product/ProductTypeSwitchingOnUpdateTest.xml b/dev/tests/functional/tests/app/Magento/Catalog/Test/TestCase/Product/ProductTypeSwitchingOnUpdateTest.xml index f3df374a8bac8..5fa1cfe5e5911 100644 --- a/dev/tests/functional/tests/app/Magento/Catalog/Test/TestCase/Product/ProductTypeSwitchingOnUpdateTest.xml +++ b/dev/tests/functional/tests/app/Magento/Catalog/Test/TestCase/Product/ProductTypeSwitchingOnUpdateTest.xml @@ -11,7 +11,6 @@ <data name="productOrigin" xsi:type="string">catalogProductSimple::default</data> <data name="product" xsi:type="string">configurableProduct::default</data> <data name="actionName" xsi:type="string">-</data> - <data name="tag" xsi:type="string">to_maintain:yes</data> <constraint name="Magento\Catalog\Test\Constraint\AssertProductSaveMessage" /> <constraint name="Magento\Catalog\Test\Constraint\AssertProductInGrid" /> <constraint name="Magento\ConfigurableProduct\Test\Constraint\AssertChildProductsInGrid" /> @@ -21,7 +20,6 @@ <constraint name="Magento\ConfigurableProduct\Test\Constraint\AssertChildProductIsNotDisplayedSeparately" /> </variation> <variation name="ProductTypeSwitchingOnUpdateTestVariation2"> - <data name="tag" xsi:type="string">to_maintain:yes</data> <data name="productOrigin" xsi:type="string">catalogProductSimple::default</data> <data name="product" xsi:type="string">catalogProductVirtual::default</data> <data name="actionName" xsi:type="string">-</data> @@ -29,7 +27,6 @@ <constraint name="Magento\Catalog\Test\Constraint\AssertProductInGrid" /> </variation> <variation name="ProductTypeSwitchingOnUpdateTestVariation3"> - <data name="tag" xsi:type="string">stable:no</data> <data name="productOrigin" xsi:type="string">configurableProduct::default</data> <data name="product" xsi:type="string">catalogProductSimple::product_without_category</data> <data name="actionName" xsi:type="string">deleteVariations</data> @@ -40,12 +37,10 @@ <data name="productOrigin" xsi:type="string">configurableProduct::default</data> <data name="product" xsi:type="string">catalogProductVirtual::required_fields</data> <data name="actionName" xsi:type="string">deleteVariations</data> - <data name="tag" xsi:type="string">to_maintain:yes</data> <constraint name="Magento\Catalog\Test\Constraint\AssertProductSaveMessage" /> <constraint name="Magento\Catalog\Test\Constraint\AssertProductInGrid" /> </variation> <variation name="ProductTypeSwitchingOnUpdateTestVariation5"> - <data name="tag" xsi:type="string">to_maintain:yes</data> <data name="productOrigin" xsi:type="string">catalogProductVirtual::default</data> <data name="product" xsi:type="string">catalogProductSimple::default</data> <data name="actionName" xsi:type="string">-</data> @@ -56,7 +51,6 @@ <data name="productOrigin" xsi:type="string">catalogProductVirtual::default</data> <data name="product" xsi:type="string">configurableProduct::not_virtual_for_type_switching</data> <data name="actionName" xsi:type="string">-</data> - <data name="tag" xsi:type="string">to_maintain:yes</data> <constraint name="Magento\Catalog\Test\Constraint\AssertProductSaveMessage" /> <constraint name="Magento\Catalog\Test\Constraint\AssertProductInGrid" /> <constraint name="Magento\ConfigurableProduct\Test\Constraint\AssertChildProductsInGrid" /> @@ -69,7 +63,6 @@ <data name="productOrigin" xsi:type="string">catalogProductVirtual::default</data> <data name="product" xsi:type="string">downloadableProduct::default</data> <data name="actionName" xsi:type="string">-</data> - <data name="tag" xsi:type="string">to_maintain:yes</data> <constraint name="Magento\Catalog\Test\Constraint\AssertProductSaveMessage" /> <constraint name="Magento\Catalog\Test\Constraint\AssertProductInGrid" /> <constraint name="Magento\Downloadable\Test\Constraint\AssertDownloadableProductForm" /> @@ -81,15 +74,13 @@ <data name="productOrigin" xsi:type="string">downloadableProduct::default</data> <data name="product" xsi:type="string">catalogProductSimple::default</data> <data name="actionName" xsi:type="string">-</data> - <data name="tag" xsi:type="string">to_maintain:yes</data> <constraint name="Magento\Catalog\Test\Constraint\AssertProductSaveMessage" /> <constraint name="Magento\Catalog\Test\Constraint\AssertProductInGrid" /> </variation> <variation name="ProductTypeSwitchingOnUpdateTestVariation9"> <data name="productOrigin" xsi:type="string">downloadableProduct::default</data> <data name="product" xsi:type="string">configurableProduct::not_virtual_for_type_switching</data> - <data name="actionName" xsi:type="string">-</data> - <data name="tag" xsi:type="string">to_maintain:yes</data> + <data name="actionName" xsi:type="string">clearDownloadableData</data> <constraint name="Magento\Catalog\Test\Constraint\AssertProductSaveMessage" /> <constraint name="Magento\Catalog\Test\Constraint\AssertProductInGrid" /> <constraint name="Magento\ConfigurableProduct\Test\Constraint\AssertChildProductsInGrid" /> @@ -99,7 +90,6 @@ <constraint name="Magento\ConfigurableProduct\Test\Constraint\AssertChildProductIsNotDisplayedSeparately" /> </variation> <variation name="ProductTypeSwitchingOnUpdateTestVariation10"> - <data name="tag" xsi:type="string">stable:no</data> <data name="productOrigin" xsi:type="string">downloadableProduct::default</data> <data name="product" xsi:type="string">catalogProductVirtual::default</data> <data name="actionName" xsi:type="string">clearDownloadableData</data> @@ -110,7 +100,6 @@ <data name="productOrigin" xsi:type="string">catalogProductSimple::default</data> <data name="product" xsi:type="string">downloadableProduct::default</data> <data name="actionName" xsi:type="string">-</data> - <data name="tag" xsi:type="string">to_maintain:yes</data> <constraint name="Magento\Catalog\Test\Constraint\AssertProductSaveMessage" /> <constraint name="Magento\Catalog\Test\Constraint\AssertProductInGrid" /> <constraint name="Magento\Downloadable\Test\Constraint\AssertDownloadableProductForm" /> diff --git a/dev/tests/functional/tests/app/Magento/Downloadable/Test/Block/Adminhtml/Catalog/Product/Edit/Section/Downloadable/Samples.php b/dev/tests/functional/tests/app/Magento/Downloadable/Test/Block/Adminhtml/Catalog/Product/Edit/Section/Downloadable/Samples.php index 98c7e1abf0d88..bcc8a012c1a63 100644 --- a/dev/tests/functional/tests/app/Magento/Downloadable/Test/Block/Adminhtml/Catalog/Product/Edit/Section/Downloadable/Samples.php +++ b/dev/tests/functional/tests/app/Magento/Downloadable/Test/Block/Adminhtml/Catalog/Product/Edit/Section/Downloadable/Samples.php @@ -114,8 +114,9 @@ protected function sortSample($position, $sortOrder, SimpleElement $element = nu foreach ($this->sortRowsData as &$sortRowData) { if ($sortRowData['sort_order'] > $currentSortRowData['sort_order']) { // need to reload block because we are changing dom - $target = $this->getRowBlock($sortRowData['current_position_in_grid'], $element)->getSortHandle(); - $this->getRowBlock($currentSortRowData['current_position_in_grid'], $element)->dragAndDropTo($target); + $target = $this->getRowBlock($currentSortRowData['current_position_in_grid'], $element) + ->getSortHandle(); + $this->getRowBlock($sortRowData['current_position_in_grid'], $element)->dragAndDropTo($target); $currentSortRowData['current_position_in_grid']--; $sortRowData['current_position_in_grid']++; diff --git a/dev/tests/functional/tests/app/Magento/Downloadable/Test/TestCase/CreateDownloadableProductEntityTest.xml b/dev/tests/functional/tests/app/Magento/Downloadable/Test/TestCase/CreateDownloadableProductEntityTest.xml index 5afe815edad45..b18f6849e0c8a 100644 --- a/dev/tests/functional/tests/app/Magento/Downloadable/Test/TestCase/CreateDownloadableProductEntityTest.xml +++ b/dev/tests/functional/tests/app/Magento/Downloadable/Test/TestCase/CreateDownloadableProductEntityTest.xml @@ -18,10 +18,10 @@ <data name="product/data/downloadable_links/dataset" xsi:type="string">one_separately_link</data> <data name="product/data/checkout_data/dataset" xsi:type="string">downloadable_one_dollar_product_with_separated_link</data> <data name="product/data/url_key" xsi:type="string">downloadableproduct-%isolation%</data> - <constraint name="Magento\Catalog\Test\Constraint\AssertProductSaveMessage" /> - <constraint name="Magento\Catalog\Test\Constraint\AssertProductVisibleInCategory" /> - <constraint name="Magento\Catalog\Test\Constraint\AssertProductPage" /> - <constraint name="Magento\Catalog\Test\Constraint\AssertProductInCart" /> + <constraint name="Magento\Catalog\Test\Constraint\AssertProductSaveMessage"/> + <constraint name="Magento\Catalog\Test\Constraint\AssertProductVisibleInCategory"/> + <constraint name="Magento\Catalog\Test\Constraint\AssertProductPage"/> + <constraint name="Magento\Catalog\Test\Constraint\AssertProductInCart"/> </variation> <variation name="CreateDownloadableProductEntityTestVariation2" summary="Create product with default set links"> <data name="product/data/name" xsi:type="string">DownloadableProduct_%isolation%</data> @@ -33,16 +33,14 @@ <data name="product/data/category" xsi:type="string">Default Category</data> <data name="product/data/downloadable_links/dataset" xsi:type="string">with_two_separately_links</data> <data name="product/data/url_key" xsi:type="string">downloadableproduct-%isolation%</data> - <constraint name="Magento\Catalog\Test\Constraint\AssertProductSaveMessage" /> - <constraint name="Magento\Catalog\Test\Constraint\AssertProductInGrid" /> - <constraint name="Magento\Downloadable\Test\Constraint\AssertDownloadableProductForm" /> - <constraint name="Magento\Catalog\Test\Constraint\AssertProductVisibleInCategory" /> - <constraint name="Magento\Catalog\Test\Constraint\AssertProductPage" /> - <constraint name="Magento\Catalog\Test\Constraint\AssertProductInStock" /> + <constraint name="Magento\Catalog\Test\Constraint\AssertProductSaveMessage"/> + <constraint name="Magento\Catalog\Test\Constraint\AssertProductInGrid"/> + <constraint name="Magento\Downloadable\Test\Constraint\AssertDownloadableProductForm"/> + <constraint name="Magento\Catalog\Test\Constraint\AssertProductVisibleInCategory"/> + <constraint name="Magento\Catalog\Test\Constraint\AssertProductPage"/> + <constraint name="Magento\Catalog\Test\Constraint\AssertProductInStock"/> </variation> <variation name="CreateDownloadableProductEntityTestVariation3" summary="Create product with default sets samples and links"> - <data name="tag" xsi:type="string">to_maintain:yes</data> - <data name="issue" xsi:type="string">MAGETWO-67096: [FT] Magento\Downloadable\Test\TestCase\CreateDownloadableProductEntityTest fails on Jenkins</data> <data name="product/data/name" xsi:type="string">DownloadableProduct_%isolation%</data> <data name="product/data/sku" xsi:type="string">DownloadableProduct_%isolation%</data> <data name="product/data/price/value" xsi:type="string">1</data> @@ -53,12 +51,12 @@ <data name="product/data/downloadable_sample/dataset" xsi:type="string">with_two_samples</data> <data name="product/data/downloadable_links/dataset" xsi:type="string">with_two_separately_links</data> <data name="product/data/url_key" xsi:type="string">downloadableproduct-%isolation%</data> - <constraint name="Magento\Catalog\Test\Constraint\AssertProductSaveMessage" /> - <constraint name="Magento\Catalog\Test\Constraint\AssertProductInGrid" /> - <constraint name="Magento\Downloadable\Test\Constraint\AssertDownloadableProductForm" /> - <constraint name="Magento\Catalog\Test\Constraint\AssertProductVisibleInCategory" /> - <constraint name="Magento\Downloadable\Test\Constraint\AssertDownloadableSamplesData" /> - <constraint name="Magento\Downloadable\Test\Constraint\AssertDownloadableLinksData" /> + <constraint name="Magento\Catalog\Test\Constraint\AssertProductSaveMessage"/> + <constraint name="Magento\Catalog\Test\Constraint\AssertProductInGrid"/> + <constraint name="Magento\Downloadable\Test\Constraint\AssertDownloadableProductForm"/> + <constraint name="Magento\Catalog\Test\Constraint\AssertProductVisibleInCategory"/> + <constraint name="Magento\Downloadable\Test\Constraint\AssertDownloadableSamplesData"/> + <constraint name="Magento\Downloadable\Test\Constraint\AssertDownloadableLinksData"/> </variation> <variation name="CreateDownloadableProductEntityTestVariation4" summary="Create product with custom options"> <data name="product/data/name" xsi:type="string">DownloadableProduct_%isolation%</data> @@ -71,16 +69,14 @@ <data name="product/data/downloadable_links/dataset" xsi:type="string">with_two_separately_links</data> <data name="product/data/custom_options/dataset" xsi:type="string">default</data> <data name="product/data/url_key" xsi:type="string">downloadableproduct-%isolation%</data> - <constraint name="Magento\Catalog\Test\Constraint\AssertProductSaveMessage" /> - <constraint name="Magento\Downloadable\Test\Constraint\AssertDownloadableProductForm" /> - <constraint name="Magento\Catalog\Test\Constraint\AssertProductCustomOptionsOnProductPage" /> - <constraint name="Magento\Catalog\Test\Constraint\AssertProductVisibleInCategory" /> - <constraint name="Magento\Catalog\Test\Constraint\AssertProductPage" /> - <constraint name="Magento\Downloadable\Test\Constraint\AssertDownloadableLinksData" /> + <constraint name="Magento\Catalog\Test\Constraint\AssertProductSaveMessage"/> + <constraint name="Magento\Downloadable\Test\Constraint\AssertDownloadableProductForm"/> + <constraint name="Magento\Catalog\Test\Constraint\AssertProductCustomOptionsOnProductPage"/> + <constraint name="Magento\Catalog\Test\Constraint\AssertProductVisibleInCategory"/> + <constraint name="Magento\Catalog\Test\Constraint\AssertProductPage"/> + <constraint name="Magento\Downloadable\Test\Constraint\AssertDownloadableLinksData"/> </variation> <variation name="CreateDownloadableProductEntityTestVariation5" summary="Create product without category"> - <data name="tag" xsi:type="string">to_maintain:yes</data> - <data name="issue" xsi:type="string">MAGETWO-67096: [FT] Magento\Downloadable\Test\TestCase\CreateDownloadableProductEntityTest fails on Jenkins</data> <data name="product/data/name" xsi:type="string">DownloadableProduct_%isolation%</data> <data name="product/data/sku" xsi:type="string">DownloadableProduct_%isolation%</data> <data name="product/data/price/value" xsi:type="string">55</data> @@ -91,15 +87,15 @@ <data name="product/data/downloadable_links/dataset" xsi:type="string">with_three_links</data> <data name="product/data/custom_options/dataset" xsi:type="string">two_options</data> <data name="product/data/url_key" xsi:type="string">downloadableproduct-%isolation%</data> - <constraint name="Magento\Catalog\Test\Constraint\AssertProductSaveMessage" /> - <constraint name="Magento\Catalog\Test\Constraint\AssertProductCustomOptionsOnProductPage" /> - <constraint name="Magento\Catalog\Test\Constraint\AssertProductInGrid" /> - <constraint name="Magento\Downloadable\Test\Constraint\AssertDownloadableProductForm" /> - <constraint name="Magento\Catalog\Test\Constraint\AssertProductVisibleInCategory" /> - <constraint name="Magento\Catalog\Test\Constraint\AssertProductPage" /> - <constraint name="Magento\Downloadable\Test\Constraint\AssertDownloadableLinksData" /> - <constraint name="Magento\Catalog\Test\Constraint\AssertProductInStock" /> - <constraint name="Magento\Catalog\Test\Constraint\AssertProductSearchableBySku" /> + <constraint name="Magento\Catalog\Test\Constraint\AssertProductSaveMessage"/> + <constraint name="Magento\Catalog\Test\Constraint\AssertProductCustomOptionsOnProductPage"/> + <constraint name="Magento\Catalog\Test\Constraint\AssertProductInGrid"/> + <constraint name="Magento\Downloadable\Test\Constraint\AssertDownloadableProductForm"/> + <constraint name="Magento\Catalog\Test\Constraint\AssertProductVisibleInCategory"/> + <constraint name="Magento\Catalog\Test\Constraint\AssertProductPage"/> + <constraint name="Magento\Downloadable\Test\Constraint\AssertDownloadableLinksData"/> + <constraint name="Magento\Catalog\Test\Constraint\AssertProductInStock"/> + <constraint name="Magento\Catalog\Test\Constraint\AssertProductSearchableBySku"/> </variation> <variation name="CreateDownloadableProductEntityTestVariation6" summary="Create product with out of stock status"> <data name="product/data/name" xsi:type="string">DownloadableProduct_%isolation%</data> @@ -111,10 +107,10 @@ <data name="product/data/category" xsi:type="string">Default Category</data> <data name="product/data/downloadable_links/dataset" xsi:type="string">with_two_separately_links</data> <data name="product/data/url_key" xsi:type="string">downloadableproduct-%isolation%</data> - <constraint name="Magento\Catalog\Test\Constraint\AssertProductSaveMessage" /> - <constraint name="Magento\Catalog\Test\Constraint\AssertProductOutOfStock" /> - <constraint name="Magento\Catalog\Test\Constraint\AssertProductInGrid" /> - <constraint name="Magento\Downloadable\Test\Constraint\AssertDownloadableProductForm" /> + <constraint name="Magento\Catalog\Test\Constraint\AssertProductSaveMessage"/> + <constraint name="Magento\Catalog\Test\Constraint\AssertProductOutOfStock"/> + <constraint name="Magento\Catalog\Test\Constraint\AssertProductInGrid"/> + <constraint name="Magento\Downloadable\Test\Constraint\AssertDownloadableProductForm"/> </variation> <variation name="CreateDownloadableProductEntityTestVariation7" summary="Create product with manage stock"> <data name="product/data/name" xsi:type="string">DownloadableProduct_%isolation%</data> @@ -127,10 +123,10 @@ <data name="product/data/stock_data/min_qty" xsi:type="string">123</data> <data name="product/data/downloadable_links/dataset" xsi:type="string">with_two_separately_links</data> <data name="product/data/url_key" xsi:type="string">downloadableproduct-%isolation%</data> - <constraint name="Magento\Catalog\Test\Constraint\AssertProductSaveMessage" /> - <constraint name="Magento\Catalog\Test\Constraint\AssertProductInGrid" /> - <constraint name="Magento\Downloadable\Test\Constraint\AssertDownloadableProductForm" /> - <constraint name="Magento\Catalog\Test\Constraint\AssertProductOutOfStock" /> + <constraint name="Magento\Catalog\Test\Constraint\AssertProductSaveMessage"/> + <constraint name="Magento\Catalog\Test\Constraint\AssertProductInGrid"/> + <constraint name="Magento\Downloadable\Test\Constraint\AssertDownloadableProductForm"/> + <constraint name="Magento\Catalog\Test\Constraint\AssertProductOutOfStock"/> </variation> <variation name="CreateDownloadableProductEntityTestVariation8" summary="Create product without tax class id"> <data name="product/data/name" xsi:type="string">DownloadableProduct_%isolation%</data> @@ -143,13 +139,12 @@ <data name="product/data/description" xsi:type="string">This is description for downloadable product</data> <data name="product/data/downloadable_links/dataset" xsi:type="string">with_two_separately_links</data> <data name="product/data/url_key" xsi:type="string">downloadableproduct-%isolation%</data> - <constraint name="Magento\Catalog\Test\Constraint\AssertProductSaveMessage" /> - <constraint name="Magento\Catalog\Test\Constraint\AssertProductInGrid" /> - <constraint name="Magento\Downloadable\Test\Constraint\AssertDownloadableProductForm" /> - <constraint name="Magento\Catalog\Test\Constraint\AssertProductPage" /> + <constraint name="Magento\Catalog\Test\Constraint\AssertProductSaveMessage"/> + <constraint name="Magento\Catalog\Test\Constraint\AssertProductInGrid"/> + <constraint name="Magento\Downloadable\Test\Constraint\AssertDownloadableProductForm"/> + <constraint name="Magento\Catalog\Test\Constraint\AssertProductPage"/> </variation> <variation name="CreateDownloadableProductEntityTestVariation9" summary="Create product with import custom options"> - <data name="issue" xsi:type="string">MAGETWO-59316: Sort order of Customizable Options isn't taken into account while creating Product via Webapi</data> <data name="product/data/name" xsi:type="string">DownloadableProduct_%isolation%</data> <data name="product/data/sku" xsi:type="string">DownloadableProduct_%isolation%</data> <data name="product/data/price/value" xsi:type="string">57</data> @@ -163,17 +158,15 @@ <data name="product/data/custom_options/dataset" xsi:type="string">default</data> <data name="product/data/custom_options/import_products" xsi:type="string">catalogProductSimple::with_two_custom_option,catalogProductSimple::with_all_custom_option</data> <data name="product/data/url_key" xsi:type="string">downloadableproduct-%isolation%</data> - <constraint name="Magento\Catalog\Test\Constraint\AssertProductSaveMessage" /> - <constraint name="Magento\Catalog\Test\Constraint\AssertProductInGrid" /> - <constraint name="Magento\Downloadable\Test\Constraint\AssertDownloadableProductForm" /> - <constraint name="Magento\Catalog\Test\Constraint\AssertProductPage" /> - <constraint name="Magento\Catalog\Test\Constraint\AssertProductCustomOptionsOnProductPage" /> - <constraint name="Magento\Downloadable\Test\Constraint\AssertDownloadableSamplesData" /> - <constraint name="Magento\Downloadable\Test\Constraint\AssertDownloadableLinksData" /> + <constraint name="Magento\Catalog\Test\Constraint\AssertProductSaveMessage"/> + <constraint name="Magento\Catalog\Test\Constraint\AssertProductInGrid"/> + <constraint name="Magento\Downloadable\Test\Constraint\AssertDownloadableProductForm"/> + <constraint name="Magento\Catalog\Test\Constraint\AssertProductPage"/> + <constraint name="Magento\Catalog\Test\Constraint\AssertProductCustomOptionsOnProductPage"/> + <constraint name="Magento\Downloadable\Test\Constraint\AssertDownloadableSamplesData"/> + <constraint name="Magento\Downloadable\Test\Constraint\AssertDownloadableLinksData"/> </variation> <variation name="CreateDownloadableProductEntityTestVariation10" summary="Create product with three links"> - <data name="tag" xsi:type="string">to_maintain:yes</data> - <data name="issue" xsi:type="string">MAGETWO-67096: [FT] Magento\Downloadable\Test\TestCase\CreateDownloadableProductEntityTest fails on Jenkins</data> <data name="product/data/name" xsi:type="string">DownloadableProduct_%isolation%</data> <data name="product/data/sku" xsi:type="string">DownloadableProduct_%isolation%</data> <data name="product/data/price/value" xsi:type="string">65</data> @@ -187,17 +180,15 @@ <data name="product/data/downloadable_links/dataset" xsi:type="string">with_three_links</data> <data name="product/data/custom_options/dataset" xsi:type="string">default</data> <data name="product/data/url_key" xsi:type="string">downloadableproduct-%isolation%</data> - <constraint name="Magento\Catalog\Test\Constraint\AssertProductSaveMessage" /> - <constraint name="Magento\Catalog\Test\Constraint\AssertProductPage" /> - <constraint name="Magento\Catalog\Test\Constraint\AssertProductInGrid" /> - <constraint name="Magento\Downloadable\Test\Constraint\AssertDownloadableProductForm" /> - <constraint name="Magento\Catalog\Test\Constraint\AssertProductCustomOptionsOnProductPage" /> - <constraint name="Magento\Downloadable\Test\Constraint\AssertDownloadableSamplesData" /> - <constraint name="Magento\Downloadable\Test\Constraint\AssertDownloadableLinksData" /> + <constraint name="Magento\Catalog\Test\Constraint\AssertProductSaveMessage"/> + <constraint name="Magento\Catalog\Test\Constraint\AssertProductPage"/> + <constraint name="Magento\Catalog\Test\Constraint\AssertProductInGrid"/> + <constraint name="Magento\Downloadable\Test\Constraint\AssertDownloadableProductForm"/> + <constraint name="Magento\Catalog\Test\Constraint\AssertProductCustomOptionsOnProductPage"/> + <constraint name="Magento\Downloadable\Test\Constraint\AssertDownloadableSamplesData"/> + <constraint name="Magento\Downloadable\Test\Constraint\AssertDownloadableLinksData"/> </variation> <variation name="CreateDownloadableProductEntityTestVariation11" summary="Create product with three links without description"> - <data name="tag" xsi:type="string">to_maintain:yes</data> - <data name="issue" xsi:type="string">MAGETWO-67096: [FT] Magento\Downloadable\Test\TestCase\CreateDownloadableProductEntityTest fails on Jenkins</data> <data name="product/data/name" xsi:type="string">DownloadableProduct_%isolation%</data> <data name="product/data/sku" xsi:type="string">DownloadableProduct_%isolation%</data> <data name="product/data/price/value" xsi:type="string">65</data> @@ -209,12 +200,12 @@ <data name="product/data/downloadable_links/dataset" xsi:type="string">with_three_links</data> <data name="product/data/custom_options/dataset" xsi:type="string">default</data> <data name="product/data/url_key" xsi:type="string">downloadableproduct-%isolation%</data> - <constraint name="Magento\Catalog\Test\Constraint\AssertProductSaveMessage" /> - <constraint name="Magento\Catalog\Test\Constraint\AssertProductInGrid" /> - <constraint name="Magento\Downloadable\Test\Constraint\AssertDownloadableProductForm" /> - <constraint name="Magento\Catalog\Test\Constraint\AssertProductPage" /> - <constraint name="Magento\Downloadable\Test\Constraint\AssertDownloadableLinksData" /> - <constraint name="Magento\Catalog\Test\Constraint\AssertProductCustomOptionsOnProductPage" /> + <constraint name="Magento\Catalog\Test\Constraint\AssertProductSaveMessage"/> + <constraint name="Magento\Catalog\Test\Constraint\AssertProductInGrid"/> + <constraint name="Magento\Downloadable\Test\Constraint\AssertDownloadableProductForm"/> + <constraint name="Magento\Catalog\Test\Constraint\AssertProductPage"/> + <constraint name="Magento\Downloadable\Test\Constraint\AssertDownloadableLinksData"/> + <constraint name="Magento\Catalog\Test\Constraint\AssertProductCustomOptionsOnProductPage"/> </variation> <variation name="CreateDownloadableProductEntityTestVariation12" summary="Create product without filling quantity and stock"> <data name="product/data/name" xsi:type="string">DownloadableProduct_%isolation%</data> @@ -225,10 +216,10 @@ <data name="product/data/category" xsi:type="string">default_category</data> <data name="product/data/downloadable_links/dataset" xsi:type="string">with_two_separately_links</data> <data name="product/data/url_key" xsi:type="string">downloadableproduct-%isolation%</data> - <constraint name="Magento\Catalog\Test\Constraint\AssertProductSaveMessage" /> - <constraint name="Magento\Downloadable\Test\Constraint\AssertDownloadableProductForm" /> - <constraint name="Magento\Catalog\Test\Constraint\AssertProductVisibleInCategory" /> - <constraint name="Magento\Catalog\Test\Constraint\AssertProductPage" /> + <constraint name="Magento\Catalog\Test\Constraint\AssertProductSaveMessage"/> + <constraint name="Magento\Downloadable\Test\Constraint\AssertDownloadableProductForm"/> + <constraint name="Magento\Catalog\Test\Constraint\AssertProductVisibleInCategory"/> + <constraint name="Magento\Catalog\Test\Constraint\AssertProductPage"/> </variation> <variation name="CreateDownloadableProductEntityTestVariation13" summary="Create product with special price"> <data name="product/data/name" xsi:type="string">DownloadableProduct_%isolation%</data> @@ -241,11 +232,11 @@ <data name="product/data/downloadable_links/dataset" xsi:type="string">with_two_separately_links</data> <data name="product/data/special_price" xsi:type="string">5</data> <data name="product/data/url_key" xsi:type="string">downloadableproduct-%isolation%</data> - <constraint name="Magento\Catalog\Test\Constraint\AssertProductSaveMessage" /> - <constraint name="Magento\Catalog\Test\Constraint\AssertProductInGrid" /> - <constraint name="Magento\Downloadable\Test\Constraint\AssertDownloadableProductForm" /> - <constraint name="Magento\Catalog\Test\Constraint\AssertProductPage" /> - <constraint name="Magento\Catalog\Test\Constraint\AssertProductSpecialPriceOnProductPage" /> + <constraint name="Magento\Catalog\Test\Constraint\AssertProductSaveMessage"/> + <constraint name="Magento\Catalog\Test\Constraint\AssertProductInGrid"/> + <constraint name="Magento\Downloadable\Test\Constraint\AssertDownloadableProductForm"/> + <constraint name="Magento\Catalog\Test\Constraint\AssertProductPage"/> + <constraint name="Magento\Catalog\Test\Constraint\AssertProductSpecialPriceOnProductPage"/> </variation> <variation name="CreateDownloadableProductEntityTestVariation14" summary="Create product with group price"> <data name="product/data/name" xsi:type="string">DownloadableProduct_%isolation%</data> @@ -257,10 +248,10 @@ <data name="product/data/category" xsi:type="string">category %isolation%</data> <data name="product/data/downloadable_links/dataset" xsi:type="string">with_two_separately_links</data> <data name="product/data/url_key" xsi:type="string">downloadableproduct-%isolation%</data> - <constraint name="Magento\Catalog\Test\Constraint\AssertProductSaveMessage" /> - <constraint name="Magento\Catalog\Test\Constraint\AssertProductInGrid" /> - <constraint name="Magento\Downloadable\Test\Constraint\AssertDownloadableProductForm" /> - <constraint name="Magento\Catalog\Test\Constraint\AssertProductPage" /> + <constraint name="Magento\Catalog\Test\Constraint\AssertProductSaveMessage"/> + <constraint name="Magento\Catalog\Test\Constraint\AssertProductInGrid"/> + <constraint name="Magento\Downloadable\Test\Constraint\AssertDownloadableProductForm"/> + <constraint name="Magento\Catalog\Test\Constraint\AssertProductPage"/> </variation> <variation name="CreateDownloadableProductEntityTestVariation15" summary="Create product with tier price"> <data name="product/data/name" xsi:type="string">DownloadableProduct_%isolation%</data> @@ -273,11 +264,11 @@ <data name="product/data/downloadable_links/dataset" xsi:type="string">with_two_separately_links</data> <data name="product/data/tier_price/dataset" xsi:type="string">default</data> <data name="product/data/url_key" xsi:type="string">downloadableproduct-%isolation%</data> - <constraint name="Magento\Catalog\Test\Constraint\AssertProductSaveMessage" /> - <constraint name="Magento\Catalog\Test\Constraint\AssertProductInGrid" /> - <constraint name="Magento\Downloadable\Test\Constraint\AssertDownloadableProductForm" /> - <constraint name="Magento\Catalog\Test\Constraint\AssertProductPage" /> - <constraint name="Magento\Catalog\Test\Constraint\AssertProductTierPriceOnProductPage" /> + <constraint name="Magento\Catalog\Test\Constraint\AssertProductSaveMessage"/> + <constraint name="Magento\Catalog\Test\Constraint\AssertProductInGrid"/> + <constraint name="Magento\Downloadable\Test\Constraint\AssertDownloadableProductForm"/> + <constraint name="Magento\Catalog\Test\Constraint\AssertProductPage"/> + <constraint name="Magento\Catalog\Test\Constraint\AssertProductTierPriceOnProductPage"/> </variation> <variation name="CreateDownloadableProductEntityTestVariation16" summary="Create downloadable product and assign it to custom website"> <data name="product/data/name" xsi:type="string">DownloadableProduct_%isolation%</data> @@ -288,8 +279,8 @@ <data name="product/data/downloadable_links/dataset" xsi:type="string">one_separately_link</data> <data name="product/data/url_key" xsi:type="string">downloadableproduct-%isolation%</data> <data name="product/data/website_ids/0/dataset" xsi:type="string">custom_store</data> - <constraint name="Magento\Catalog\Test\Constraint\AssertProductSaveMessage" /> - <constraint name="Magento\Catalog\Test\Constraint\AssertProductOnCustomWebsite" /> + <constraint name="Magento\Catalog\Test\Constraint\AssertProductSaveMessage"/> + <constraint name="Magento\Catalog\Test\Constraint\AssertProductOnCustomWebsite"/> </variation> </testCase> </config> diff --git a/dev/tests/functional/tests/app/Magento/GroupedProduct/Test/Fixture/GroupedProduct.xml b/dev/tests/functional/tests/app/Magento/GroupedProduct/Test/Fixture/GroupedProduct.xml index dce1358a1ecf4..59c00683e3b1a 100644 --- a/dev/tests/functional/tests/app/Magento/GroupedProduct/Test/Fixture/GroupedProduct.xml +++ b/dev/tests/functional/tests/app/Magento/GroupedProduct/Test/Fixture/GroupedProduct.xml @@ -58,7 +58,7 @@ <field name="sku" is_required="1" group="product-details" /> <field name="small_image" is_required="0" /> <field name="small_image_label" is_required="0" /> - <field name="status" is_required="0" /> + <field name="status" is_required="0" group="product-details" /> <field name="thumbnail" is_required="0" /> <field name="thumbnail_label" is_required="0" /> <field name="updated_at" is_required="1" /> @@ -66,7 +66,7 @@ <field name="upsell_tgtr_position_limit" is_required="0" /> <field name="url_key" is_required="0" group="search-engine-optimization" /> <field name="url_path" is_required="0" /> - <field name="visibility" is_required="0" /> + <field name="visibility" is_required="0" group="product-details" /> <field name="id" /> <field name="type_id" /> <field name="attribute_set_id" group="product-details" source="Magento\Catalog\Test\Fixture\Product\AttributeSetId" /> diff --git a/dev/tests/functional/tests/app/Magento/GroupedProduct/Test/TestCase/CreateGroupedProductEntityTest.xml b/dev/tests/functional/tests/app/Magento/GroupedProduct/Test/TestCase/CreateGroupedProductEntityTest.xml index 38ef02ff49441..39f4fd08bb922 100644 --- a/dev/tests/functional/tests/app/Magento/GroupedProduct/Test/TestCase/CreateGroupedProductEntityTest.xml +++ b/dev/tests/functional/tests/app/Magento/GroupedProduct/Test/TestCase/CreateGroupedProductEntityTest.xml @@ -8,7 +8,7 @@ <config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="../../../../../../vendor/magento/mtf/etc/variations.xsd"> <testCase name="Magento\GroupedProduct\Test\TestCase\CreateGroupedProductEntityTest" summary="Create Grouped Product" ticketId="MAGETWO-24877"> <variation name="CreateGroupedProductEntityTestVariation1" summary="Create Grouped Product and Assign It to the Category" ticketId="MAGETWO-13610"> - <data name="tag" xsi:type="string">test_type:acceptance_test, test_type:extended_acceptance_test, stable:no</data> + <data name="tag" xsi:type="string">test_type:acceptance_test, test_type:extended_acceptance_test</data> <data name="product/data/url_key" xsi:type="string">test-grouped-product-%isolation%</data> <data name="product/data/name" xsi:type="string">GroupedProduct %isolation%</data> <data name="product/data/sku" xsi:type="string">GroupedProduct_sku%isolation%</data> diff --git a/dev/tests/functional/tests/app/Magento/Reports/Test/TestCase/SalesOrderReportEntityTest.xml b/dev/tests/functional/tests/app/Magento/Reports/Test/TestCase/SalesOrderReportEntityTest.xml index 998102bcfbc45..02bfacae43730 100644 --- a/dev/tests/functional/tests/app/Magento/Reports/Test/TestCase/SalesOrderReportEntityTest.xml +++ b/dev/tests/functional/tests/app/Magento/Reports/Test/TestCase/SalesOrderReportEntityTest.xml @@ -8,6 +8,7 @@ <config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="../../../../../../vendor/magento/mtf/etc/variations.xsd"> <testCase name="Magento\Reports\Test\TestCase\SalesOrderReportEntityTest" summary="Sales Order Report" ticketId="MAGETWO-29136"> <variation name="SalesOrderReportEntityTestVariation1" summary="Create sales order report, check report data and date" ticketId="MAGETWO-45399"> + <data name="issue" xsi:type="string">MAGETWO-97404: 2019-01-02: Tests fail Magento\Reports\Test\TestCase\SalesOrderReportEntityTest</data> <data name="order/dataset" xsi:type="string">default</data> <data name="order/data/price/dataset" xsi:type="string">full_invoice</data> <data name="salesReport/report_type" xsi:type="string">Order Created</data> diff --git a/dev/tests/functional/tests/app/Magento/Reports/Test/TestCase/SalesRefundsReportEntityTest.xml b/dev/tests/functional/tests/app/Magento/Reports/Test/TestCase/SalesRefundsReportEntityTest.xml index 27014c0afb375..3142eec7079ba 100644 --- a/dev/tests/functional/tests/app/Magento/Reports/Test/TestCase/SalesRefundsReportEntityTest.xml +++ b/dev/tests/functional/tests/app/Magento/Reports/Test/TestCase/SalesRefundsReportEntityTest.xml @@ -8,6 +8,7 @@ <config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="../../../../../../vendor/magento/mtf/etc/variations.xsd"> <testCase name="Magento\Reports\Test\TestCase\SalesRefundsReportEntityTest" summary="Sales Refunds Report" ticketId="MAGETWO-29348"> <variation name="SalesRefundsReportEntityTestVariation1"> + <data name="issue" xsi:type="string">MAGETWO-97404: 2019-01-02: Tests fail Magento\Reports\Test\TestCase\SalesOrderReportEntityTest</data> <data name="description" xsi:type="string">assert refunds year report</data> <data name="order/dataset" xsi:type="string">default</data> <data name="order/data/price/dataset" xsi:type="string">full_invoice</data> diff --git a/dev/tests/functional/tests/app/Magento/Sales/Test/Block/Adminhtml/Order/AbstractForm.php b/dev/tests/functional/tests/app/Magento/Sales/Test/Block/Adminhtml/Order/AbstractForm.php index 5572c06816a39..bc7ee4372d61b 100644 --- a/dev/tests/functional/tests/app/Magento/Sales/Test/Block/Adminhtml/Order/AbstractForm.php +++ b/dev/tests/functional/tests/app/Magento/Sales/Test/Block/Adminhtml/Order/AbstractForm.php @@ -6,7 +6,9 @@ namespace Magento\Sales\Test\Block\Adminhtml\Order; +use function GuzzleHttp\Psr7\str; use Magento\Mtf\Block\Form; +use Magento\Mtf\Client\Locator; /** * Abstract Form block. @@ -42,6 +44,27 @@ function () { ); } + /** + * Wait for element is enabled. + * + * @param string $selector + * @param string $strategy + * @return bool|null + */ + protected function waitForElementEnabled(string $selector, string $strategy = Locator::SELECTOR_CSS) + { + $browser = $this->browser; + + return $browser->waitUntil( + function () use ($browser, $selector, $strategy) { + $element = $browser->find($selector, $strategy); + $class = $element->getAttribute('class'); + + return (!$element->isDisabled() && !strpos($class, 'disabled')) ? true : null; + } + ); + } + /** * Fill form data. * @@ -113,7 +136,7 @@ abstract protected function getItemsBlock(); */ public function submit() { - $this->waitLoader(); + $this->waitForElementEnabled($this->send); $this->_rootElement->find($this->send)->click(); } diff --git a/dev/tests/functional/tests/app/Magento/Sales/Test/Constraint/AssertOrderCancelMassActionFailMessage.php b/dev/tests/functional/tests/app/Magento/Sales/Test/Constraint/AssertOrderCancelMassActionFailMessage.php index 6d3b99ce81a04..9f4181b70e801 100644 --- a/dev/tests/functional/tests/app/Magento/Sales/Test/Constraint/AssertOrderCancelMassActionFailMessage.php +++ b/dev/tests/functional/tests/app/Magento/Sales/Test/Constraint/AssertOrderCancelMassActionFailMessage.php @@ -11,17 +11,17 @@ /** * Class AssertOrderCancelMassActionFailMessage - * Assert cancel fail message is displayed on order index page + * Assert cancel fail message is displayed on order index page. */ class AssertOrderCancelMassActionFailMessage extends AbstractConstraint { /** - * Text value to be checked + * Text value to be checked. */ - const FAIL_CANCEL_MESSAGE = '1 order(s) cannot be canceled.'; + const FAIL_CANCEL_MESSAGE = 'You cannot cancel the order(s).'; /** - * Assert cancel fail message is displayed on order index page + * Assert cancel fail message is displayed on order index page. * * @param OrderIndex $orderIndex * @return void @@ -35,7 +35,7 @@ public function processAssert(OrderIndex $orderIndex) } /** - * Returns a string representation of the object + * Returns a string representation of the object. * * @return string */ diff --git a/dev/tests/functional/tests/app/Magento/Sales/Test/Constraint/AssertOrderStatusIsCorrect.php b/dev/tests/functional/tests/app/Magento/Sales/Test/Constraint/AssertOrderStatusIsCorrect.php index 46f6ebba51fe1..7a46d0c5ef820 100644 --- a/dev/tests/functional/tests/app/Magento/Sales/Test/Constraint/AssertOrderStatusIsCorrect.php +++ b/dev/tests/functional/tests/app/Magento/Sales/Test/Constraint/AssertOrderStatusIsCorrect.php @@ -39,8 +39,8 @@ public function processAssert( /** @var \Magento\Sales\Test\Block\Adminhtml\Order\View\Tab\Info $infoTab */ $infoTab = $salesOrderView->getOrderForm()->openTab('info')->getTab('info'); \PHPUnit_Framework_Assert::assertEquals( - $infoTab->getOrderStatus(), - $orderStatus + $orderStatus, + $infoTab->getOrderStatus() ); } diff --git a/dev/tests/functional/tests/app/Magento/Sales/Test/TestCase/GridSortingTest.xml b/dev/tests/functional/tests/app/Magento/Sales/Test/TestCase/GridSortingTest.xml index 6ae9d19a898bc..28894ed6cc158 100644 --- a/dev/tests/functional/tests/app/Magento/Sales/Test/TestCase/GridSortingTest.xml +++ b/dev/tests/functional/tests/app/Magento/Sales/Test/TestCase/GridSortingTest.xml @@ -9,7 +9,6 @@ <testCase name="Magento\Ui\Test\TestCase\GridSortingTest" summary="Grid UI Component Sorting" ticketId="MAGETWO-41328"> <variation name="SalesOrderGridSorting"> <data name="tag" xsi:type="string">severity:S2</data> - <data name="tag" xsi:type="string">to_maintain:yes</data> <data name="description" xsi:type="string">Verify sales order grid storting</data> <data name="steps" xsi:type="array"> <item name="0" xsi:type="string">-</item> diff --git a/dev/tests/functional/tests/app/Magento/Sales/Test/TestCase/MassOrdersUpdateTest.xml b/dev/tests/functional/tests/app/Magento/Sales/Test/TestCase/MassOrdersUpdateTest.xml index eddc321e9ca52..1f75b07c8ca1e 100644 --- a/dev/tests/functional/tests/app/Magento/Sales/Test/TestCase/MassOrdersUpdateTest.xml +++ b/dev/tests/functional/tests/app/Magento/Sales/Test/TestCase/MassOrdersUpdateTest.xml @@ -8,7 +8,6 @@ <config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="../../../../../../vendor/magento/mtf/etc/variations.xsd"> <testCase name="Magento\Sales\Test\TestCase\MassOrdersUpdateTest" summary="Mass Update Orders" ticketId="MAGETWO-27897"> <variation name="MassOrdersUpdateTestVariation1"> - <data name="tag" xsi:type="string">stable:no</data> <data name="description" xsi:type="string">cancel orders in status Pending and Processing</data> <data name="steps" xsi:type="string">-</data> <data name="action" xsi:type="string">Cancel</data> @@ -18,20 +17,20 @@ <constraint name="Magento\Sales\Test\Constraint\AssertOrdersInOrdersGrid" /> </variation> <variation name="MassOrdersUpdateTestVariation2"> - <data name="description" xsi:type="string">try to cancel orders in status Complete, Canceled</data> + <data name="description" xsi:type="string">try to cancel orders in status Complete, Closed</data> <data name="steps" xsi:type="string">invoice, shipment|invoice, credit memo</data> <data name="action" xsi:type="string">Cancel</data> <data name="ordersCount" xsi:type="string">2</data> - <data name="resultStatuses" xsi:type="string">Complete,Canceled</data> + <data name="resultStatuses" xsi:type="string">Complete,Closed</data> <constraint name="Magento\Sales\Test\Constraint\AssertOrderCancelMassActionFailMessage" /> <constraint name="Magento\Sales\Test\Constraint\AssertOrdersInOrdersGrid" /> </variation> <variation name="MassOrdersUpdateTestVariation3"> - <data name="description" xsi:type="string">try to cancel orders in status Pending, Closed</data> + <data name="description" xsi:type="string">try to cancel orders in status Processing, Closed</data> <data name="steps" xsi:type="string">invoice|invoice, credit memo</data> <data name="action" xsi:type="string">Cancel</data> <data name="ordersCount" xsi:type="string">2</data> - <data name="resultStatuses" xsi:type="string">Processing,Canceled</data> + <data name="resultStatuses" xsi:type="string">Processing,Closed</data> <constraint name="Magento\Sales\Test\Constraint\AssertOrderCancelMassActionFailMessage" /> <constraint name="Magento\Sales\Test\Constraint\AssertOrdersInOrdersGrid" /> </variation> @@ -45,7 +44,6 @@ <constraint name="Magento\Sales\Test\Constraint\AssertOrdersInOrdersGrid" /> </variation> <variation name="MassOrdersUpdateTestVariation5"> - <data name="tag" xsi:type="string">stable:no</data> <data name="description" xsi:type="string">Try to put order in status Complete on Hold</data> <data name="steps" xsi:type="string">invoice, shipment</data> <data name="action" xsi:type="string">Hold</data> diff --git a/dev/tests/functional/tests/app/Magento/Sales/Test/TestCase/MoveLastOrderedProductsOnOrderPageTest.xml b/dev/tests/functional/tests/app/Magento/Sales/Test/TestCase/MoveLastOrderedProductsOnOrderPageTest.xml index 6f568df8f21ca..a9bd3fcacea8a 100644 --- a/dev/tests/functional/tests/app/Magento/Sales/Test/TestCase/MoveLastOrderedProductsOnOrderPageTest.xml +++ b/dev/tests/functional/tests/app/Magento/Sales/Test/TestCase/MoveLastOrderedProductsOnOrderPageTest.xml @@ -14,7 +14,6 @@ <constraint name="Magento\Sales\Test\Constraint\AssertProductInItemsOrderedGrid" /> </variation> <variation name="MoveLastOrderedProductsOnOrderPageTestVariation2"> - <data name="issue" xsi:type="string">MAGETWO-58762: Customer grid does not open in MoveLastOrderedProductsOnOrderPageTestVariation2 on Jenkins</data> <data name="order/dataset" xsi:type="string">default</data> <data name="order/data/entity_id/products" xsi:type="string">configurableProduct::configurable_with_qty_1</data> <constraint name="Magento\Sales\Test\Constraint\AssertProductInItemsOrderedGrid" /> diff --git a/dev/tests/functional/tests/app/Magento/SalesRule/Test/Block/Adminhtml/Promo/Quote/Edit/PromoQuoteForm.php b/dev/tests/functional/tests/app/Magento/SalesRule/Test/Block/Adminhtml/Promo/Quote/Edit/PromoQuoteForm.php index 6a262798746a6..54cec6cf279f6 100644 --- a/dev/tests/functional/tests/app/Magento/SalesRule/Test/Block/Adminhtml/Promo/Quote/Edit/PromoQuoteForm.php +++ b/dev/tests/functional/tests/app/Magento/SalesRule/Test/Block/Adminhtml/Promo/Quote/Edit/PromoQuoteForm.php @@ -28,6 +28,13 @@ class PromoQuoteForm extends FormSections */ protected $waitForSelectorVisible = false; + /** + * Selector of name element on the form. + * + * @var string + */ + private $nameElementSelector = 'input[name=name]'; + /** * Fill form with sections. * @@ -39,6 +46,7 @@ class PromoQuoteForm extends FormSections public function fill(FixtureInterface $fixture, SimpleElement $element = null, array $replace = null) { $this->waitForElementNotVisible($this->waitForSelector); + $this->waitForElementVisible($this->nameElementSelector); $sections = $this->getFixtureFieldsByContainers($fixture); if ($replace) { $sections = $this->prepareData($sections, $replace); diff --git a/dev/tests/functional/tests/app/Magento/Search/Test/TestCase/AdvancedSearchWithAttributeTest.xml b/dev/tests/functional/tests/app/Magento/Search/Test/TestCase/AdvancedSearchWithAttributeTest.xml index 13c7051d0c1ba..733b110ec5494 100644 --- a/dev/tests/functional/tests/app/Magento/Search/Test/TestCase/AdvancedSearchWithAttributeTest.xml +++ b/dev/tests/functional/tests/app/Magento/Search/Test/TestCase/AdvancedSearchWithAttributeTest.xml @@ -8,8 +8,6 @@ <config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="../../../../../../vendor/magento/mtf/etc/variations.xsd"> <testCase name="Magento\Search\Test\TestCase\AdvancedSearchWithAttributeTest" summary="Use Advanced Search by Decimal indexable attribute if Edit/Add Attribute" ticketId="MAGETWO-25931"> <variation name="AdvancedSearchWithWeightAttributeTestVariation1"> - <data name="issue" xsi:type="string">MAGETWO-65408: [FT] Magento\Search\Test\TestCase\AdvancedSearchWithAttributeTest fails on Jenkins</data> - <data name="tag" xsi:type="string">to_maintain:yes</data> <data name="productDropDownList/0" xsi:type="string">configurable</data> <data name="productDropDownList/1" xsi:type="string">simple</data> <data name="productDropDownList/2" xsi:type="string">bundle</data> diff --git a/dev/tests/functional/tests/app/Magento/Shipping/Test/TestCase/SalesShippingReportEntityTest.xml b/dev/tests/functional/tests/app/Magento/Shipping/Test/TestCase/SalesShippingReportEntityTest.xml index 069bf23f5f25f..6b4c14ff3142a 100644 --- a/dev/tests/functional/tests/app/Magento/Shipping/Test/TestCase/SalesShippingReportEntityTest.xml +++ b/dev/tests/functional/tests/app/Magento/Shipping/Test/TestCase/SalesShippingReportEntityTest.xml @@ -8,6 +8,7 @@ <config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="../../../../../../vendor/magento/mtf/etc/variations.xsd"> <testCase name="Magento\Shipping\Test\TestCase\SalesShippingReportEntityTest" summary="Sales Shipping Report" ticketId="MAGETWO-40914"> <variation name="SalesShippingReportEntityTestVariation1"> + <data name="issue" xsi:type="string">MAGETWO-97404: 2019-01-02: Tests fail Magento\Reports\Test\TestCase\SalesOrderReportEntityTest</data> <data name="order/dataset" xsi:type="string">default</data> <data name="order/data/price/dataset" xsi:type="string">full_shipment</data> <data name="shippingReport/report_type" xsi:type="string">Order Created</data> diff --git a/dev/tests/functional/tests/app/Magento/Wishlist/Test/TestCase/AddProductToWishlistEntityTest.xml b/dev/tests/functional/tests/app/Magento/Wishlist/Test/TestCase/AddProductToWishlistEntityTest.xml index eaffe2485f222..dadf97d7940bd 100644 --- a/dev/tests/functional/tests/app/Magento/Wishlist/Test/TestCase/AddProductToWishlistEntityTest.xml +++ b/dev/tests/functional/tests/app/Magento/Wishlist/Test/TestCase/AddProductToWishlistEntityTest.xml @@ -110,6 +110,7 @@ <data name="product" xsi:type="array"> <item name="0" xsi:type="string">bundleProduct::with_special_price_and_custom_options</item> </data> + <data name="configure" xsi:type="boolean">false</data> <constraint name="Magento\Wishlist\Test\Constraint\AssertAddProductToWishlistSuccessMessage"/> <constraint name="Magento\Wishlist\Test\Constraint\AssertProductIsPresentInWishlist"/> <constraint name="Magento\Wishlist\Test\Constraint\AssertProductIsPresentInCustomerBackendWishlist"/> diff --git a/dev/tests/integration/framework/Magento/TestFramework/TestCase/AbstractBackendController.php b/dev/tests/integration/framework/Magento/TestFramework/TestCase/AbstractBackendController.php index 543bac2c6b5b5..0845ef640aa0c 100644 --- a/dev/tests/integration/framework/Magento/TestFramework/TestCase/AbstractBackendController.php +++ b/dev/tests/integration/framework/Magento/TestFramework/TestCase/AbstractBackendController.php @@ -98,7 +98,7 @@ public function testAclHasAccess() public function testAclNoAccess() { - if ($this->resource === null) { + if ($this->resource === null || $this->uri === null) { $this->markTestIncomplete('Acl test is not complete'); } $this->_objectManager->get(\Magento\Framework\Acl\Builder::class) diff --git a/dev/tests/integration/testsuite/Magento/Braintree/Controller/Paypal/PlaceOrderTest.php b/dev/tests/integration/testsuite/Magento/Braintree/Controller/Paypal/PlaceOrderTest.php new file mode 100644 index 0000000000000..4f2b0fd67840d --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Braintree/Controller/Paypal/PlaceOrderTest.php @@ -0,0 +1,173 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Braintree\Controller\Paypal; + +use Braintree\Result\Successful; +use Braintree\Transaction; +use Magento\Braintree\Model\Adapter\BraintreeAdapter; +use Magento\Braintree\Model\Adapter\BraintreeAdapterFactory; +use Magento\Checkout\Model\Session; +use Magento\Framework\Api\SearchCriteriaBuilder; +use Magento\Framework\App\Request\Http as HttpRequest; +use Magento\Framework\Message\MessageInterface; +use Magento\Quote\Api\CartRepositoryInterface; +use Magento\Quote\Api\Data\CartInterface; +use Magento\Sales\Api\Data\OrderInterface; +use Magento\Sales\Api\OrderRepositoryInterface; +use Magento\TestFramework\TestCase\AbstractController; +use PHPUnit\Framework\MockObject\MockObject; + +/** + * PlaceOrderTest + * @SuppressWarnings(PHPMD.CouplingBetweenObjects) + */ +class PlaceOrderTest extends AbstractController +{ + /** + * @var Session|MockObject + */ + private $session; + + /** + * @var BraintreeAdapter|MockObject + */ + private $adapter; + + /** + * @inheritdoc + */ + protected function setUp() + { + parent::setUp(); + + $this->session = $this->getMockBuilder(Session::class) + ->disableOriginalConstructor() + ->setMethods(['getQuote', 'setLastOrderStatus', 'unsLastBillingAgreementReferenceId']) + ->getMock(); + + $adapterFactory = $this->getMockBuilder(BraintreeAdapterFactory::class) + ->disableOriginalConstructor() + ->getMock(); + $this->adapter = $this->getMockBuilder(BraintreeAdapter::class) + ->disableOriginalConstructor() + ->getMock(); + $adapterFactory->method('create') + ->willReturn($this->adapter); + + $this->_objectManager->addSharedInstance($this->session, Session::class); + $this->_objectManager->addSharedInstance($adapterFactory, BraintreeAdapterFactory::class); + } + + /** + * @inheritdoc + */ + protected function tearDown() + { + $this->_objectManager->removeSharedInstance(Session::class); + $this->_objectManager->removeSharedInstance(BraintreeAdapterFactory::class); + parent::tearDown(); + } + + /** + * Tests a negative scenario for a place order flow when exception throws after placing an order. + * + * @magentoDataFixture Magento/Braintree/Fixtures/paypal_quote.php + */ + public function testExecuteWithFailedOrder() + { + $reservedOrderId = 'test01'; + $quote = $this->getQuote($reservedOrderId); + + $this->session->method('getQuote') + ->willReturn($quote); + + $this->adapter->method('sale') + ->willReturn($this->getTransactionStub('authorized')); + $this->adapter->method('void') + ->willReturn($this->getTransactionStub('voided')); + + // emulates an error after placing the order + $this->session->method('setLastOrderStatus') + ->willThrowException(new \Exception('Test Exception')); + + $this->getRequest()->setMethod(HttpRequest::METHOD_POST); + $this->dispatch('braintree/paypal/placeOrder'); + + self::assertRedirect(self::stringContains('checkout/cart')); + self::assertSessionMessages( + self::equalTo(['The order #' . $reservedOrderId . ' cannot be processed.']), + MessageInterface::TYPE_ERROR + ); + + $order = $this->getOrder($reservedOrderId); + self::assertEquals('canceled', $order->getState()); + } + + /** + * Gets quote by reserved order ID. + * + * @param string $reservedOrderId + * @return CartInterface + */ + private function getQuote(string $reservedOrderId): CartInterface + { + $searchCriteria = $this->_objectManager->get(SearchCriteriaBuilder::class) + ->addFilter('reserved_order_id', $reservedOrderId) + ->create(); + + /** @var CartRepositoryInterface $quoteRepository */ + $quoteRepository = $this->_objectManager->get(CartRepositoryInterface::class); + $items = $quoteRepository->getList($searchCriteria) + ->getItems(); + + return array_pop($items); + } + + /** + * Gets order by increment ID. + * + * @param string $incrementId + * @return OrderInterface + */ + private function getOrder(string $incrementId): OrderInterface + { + $searchCriteria = $this->_objectManager->get(SearchCriteriaBuilder::class) + ->addFilter('increment_id', $incrementId) + ->create(); + + /** @var OrderRepositoryInterface $repository */ + $repository = $this->_objectManager->get(OrderRepositoryInterface::class); + $items = $repository->getList($searchCriteria) + ->getItems(); + + return array_pop($items); + } + + /** + * Creates stub for Braintree Transaction. + * + * @param string $status + * @return Successful + */ + private function getTransactionStub(string $status): Successful + { + $transaction = $this->getMockBuilder(Transaction::class) + ->disableOriginalConstructor() + ->getMock(); + $transaction->status = $status; + $transaction->paypal = [ + 'paymentId' => 'pay-001', + 'payerEmail' => 'test@test.com' + ]; + $response = new Successful(); + $response->success = true; + $response->transaction = $transaction; + + return $response; + } +} diff --git a/dev/tests/integration/testsuite/Magento/Braintree/Fixtures/paypal_quote.php b/dev/tests/integration/testsuite/Magento/Braintree/Fixtures/paypal_quote.php new file mode 100644 index 0000000000000..d0db6233f05b1 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Braintree/Fixtures/paypal_quote.php @@ -0,0 +1,29 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +use Magento\Braintree\Model\Ui\PayPal\ConfigProvider; +use Magento\Quote\Api\CartRepositoryInterface; + +require __DIR__ . '/../_files/paypal_vault_token.php'; +require __DIR__ . '/../../Sales/_files/quote_with_customer.php'; + +$quote->getShippingAddress() + ->setShippingMethod('flatrate_flatrate') + ->setCollectShippingRates(true); +$quote->getPayment() + ->setMethod(ConfigProvider::PAYPAL_VAULT_CODE) + ->setAdditionalInformation( + [ + 'customer_id' => $quote->getCustomerId(), + 'public_hash' => $paymentToken->getPublicHash() + ] + ); + +$quote->collectTotals(); + +/** @var CartRepositoryInterface $quoteRepository */ +$quoteRepository = $objectManager->get(CartRepositoryInterface::class); +$quoteRepository->save($quote); diff --git a/dev/tests/integration/testsuite/Magento/Braintree/Fixtures/paypal_quote_rollback.php b/dev/tests/integration/testsuite/Magento/Braintree/Fixtures/paypal_quote_rollback.php new file mode 100644 index 0000000000000..5d0fa8cca85d9 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Braintree/Fixtures/paypal_quote_rollback.php @@ -0,0 +1,9 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +declare(strict_types=1); + +require __DIR__ . '/../../Sales/_files/quote_with_customer_rollback.php'; diff --git a/dev/tests/integration/testsuite/Magento/Catalog/Controller/Adminhtml/CategoryTest.php b/dev/tests/integration/testsuite/Magento/Catalog/Controller/Adminhtml/CategoryTest.php index bfc7e33d5f771..a6cffda80e705 100644 --- a/dev/tests/integration/testsuite/Magento/Catalog/Controller/Adminhtml/CategoryTest.php +++ b/dev/tests/integration/testsuite/Magento/Catalog/Controller/Adminhtml/CategoryTest.php @@ -5,11 +5,35 @@ */ namespace Magento\Catalog\Controller\Adminhtml; +use Magento\TestFramework\Helper\Bootstrap; +use Magento\Store\Model\Store; +use Magento\Catalog\Model\ResourceModel\Product; + /** * @magentoAppArea adminhtml */ class CategoryTest extends \Magento\TestFramework\TestCase\AbstractBackendController { + /** + * @var \Magento\Catalog\Model\ResourceModel\Product + */ + protected $productResource; + + /** + * @inheritDoc + * + * @throws \Magento\Framework\Exception\AuthenticationException + */ + protected function setUp() + { + parent::setUp(); + + /** @var Product $productResource */ + $this->productResource = Bootstrap::getObjectManager()->get( + Product::class + ); + } + /** * @magentoDataFixture Magento/Store/_files/core_fixturestore.php * @magentoDbIsolation enabled @@ -123,6 +147,9 @@ public static function categoryCreatedFromProductCreationPageDataProvider() return [[$postData], [$postData + ['return_session_messages_only' => 1]]]; } + /** + * Test SuggestCategories finds any categories. + */ public function testSuggestCategoriesActionDefaultCategoryFound() { $this->getRequest()->setParam('label_part', 'Default'); @@ -133,6 +160,9 @@ public function testSuggestCategoriesActionDefaultCategoryFound() ); } + /** + * Test SuggestCategories properly processes search by label. + */ public function testSuggestCategoriesActionNoSuggestions() { $this->getRequest()->setParam('label_part', strrev('Default')); @@ -322,6 +352,9 @@ public function saveActionDataProvider() ]; } + /** + * Test validation. + */ public function testSaveActionCategoryWithDangerRequest() { $this->getRequest()->setPostValue( @@ -388,9 +421,132 @@ public function moveActionDataProvider() { return [ [400, 401, 'first_url_key', 402, 'second_url_key', false], - [400, 401, 'duplicated_url_key', 402, 'duplicated_url_key', true], + [400, 401, 'duplicated_url_key', 402, 'duplicated_url_key', false], [0, 401, 'first_url_key', 402, 'second_url_key', true], [400, 401, 'first_url_key', 0, 'second_url_key', true], ]; } + + /** + * @magentoDataFixture Magento/Catalog/_files/products_in_different_stores.php + * @magentoDbIsolation disabled + * @dataProvider saveActionWithDifferentWebsitesDataProvider + * + * @param array $postData + */ + public function testSaveCategoryWithProductPosition(array $postData) + { + /** @var $store \Magento\Store\Model\Store */ + $store = Bootstrap::getObjectManager()->create(Store::class); + $store->load('fixturestore', 'code'); + $storeId = $store->getId(); + $oldCategoryProductsCount = $this->getCategoryProductsCount(); + $this->getRequest()->setParam('store', $storeId); + $this->getRequest()->setParam('id', 96377); + $this->getRequest()->setPostValue($postData); + $this->dispatch('backend/catalog/category/save'); + $newCategoryProductsCount = $this->getCategoryProductsCount(); + $this->assertEquals( + $oldCategoryProductsCount, + $newCategoryProductsCount, + 'After changing product position number of records from catalog_category_product has changed' + ); + } + + /** + * @SuppressWarnings(PHPMD.ExcessiveMethodLength) + * @return array + */ + public function saveActionWithDifferentWebsitesDataProvider() + { + return [ + 'default_values' => [ + [ + 'store_id' => '1', + 'entity_id' => '96377', + 'attribute_set_id' => '4', + 'parent_id' => '2', + 'created_at' => '2018-11-29 08:28:37', + 'updated_at' => '2018-11-29 08:57:43', + 'path' => '1/2/96377', + 'level' => '2', + 'children_count' => '0', + 'row_id' => '96377', + 'name' => 'Category 1', + 'display_mode' => 'PRODUCTS', + 'url_key' => 'category-1', + 'url_path' => 'category-1', + 'automatic_sorting' => '0', + 'is_active' => '1', + 'is_anchor' => '1', + 'include_in_menu' => '1', + 'custom_use_parent_settings' => '0', + 'custom_apply_to_products' => '0', + 'path_ids' => [ + 0 => '1', + 1 => '2', + 2 => '96377' + ], + 'use_config' => [ + 'available_sort_by' => 'true', + 'default_sort_by' => 'true', + 'filter_price_range' => 'true' + ], + 'id' => '', + 'parent' => '0', + 'use_default' => [ + 'name' => '1', + 'url_key' => '1', + 'meta_title' => '1', + 'is_active' => '1', + 'include_in_menu' => '1', + 'custom_use_parent_settings' => '1', + 'custom_apply_to_products' => '1', + 'description' => '1', + 'landing_page' => '1', + 'display_mode' => '1', + 'custom_design' => '1', + 'page_layout' => '1', + 'meta_keywords' => '1', + 'meta_description' => '1', + 'custom_layout_update' => '1', + 'image' => '1' + ], + 'filter_price_range' => false, + 'meta_title' => false, + 'url_key_create_redirect' => 'category-1', + 'description' => false, + 'landing_page' => false, + 'default_sort_by' => 'position', + 'available_sort_by' => false, + 'custom_design' => false, + 'page_layout' => false, + 'meta_keywords' => false, + 'meta_description' => false, + 'custom_layout_update' => false, + 'position_cache_key' => '5c069248346ac', + 'is_smart_category' => '0', + 'smart_category_rules' => false, + 'sort_order' => '0', + 'vm_category_products' => '{"1":1,"3":0}' + ] + ] + ]; + } + + /** + * Get items count from catalog_category_product + * + * @return int + */ + private function getCategoryProductsCount(): int + { + $oldCategoryProducts = $this->productResource->getConnection()->select()->from( + $this->productResource->getTable('catalog_category_product'), + 'product_id' + ); + return count( + $this->productResource->getConnection()->fetchAll($oldCategoryProducts) + ); + } } diff --git a/dev/tests/integration/testsuite/Magento/Catalog/Model/Indexer/Product/Flat/Action/CustomFlatAttributeTest.php b/dev/tests/integration/testsuite/Magento/Catalog/Model/Indexer/Product/Flat/Action/CustomFlatAttributeTest.php new file mode 100644 index 0000000000000..763237c702933 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Catalog/Model/Indexer/Product/Flat/Action/CustomFlatAttributeTest.php @@ -0,0 +1,221 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Catalog\Model\Indexer\Product\Flat\Action; + +use Magento\Store\Api\StoreRepositoryInterface; +use Magento\TestFramework\Indexer\TestCase as IndexerTestCase; +use Magento\TestFramework\Helper\Bootstrap; +use Magento\Catalog\Api\ProductRepositoryInterface; +use Magento\Catalog\Model\Indexer\Product\Flat\Processor; +use Magento\Catalog\Model\ResourceModel\Product\Flat as FlatResource; +use Magento\Framework\Api\SearchCriteriaBuilder; +use Magento\Catalog\Api\ProductAttributeRepositoryInterface; +use Magento\Store\Model\Store; +use Magento\Catalog\Model\ResourceModel\Product\Collection; +use Magento\Catalog\Model\Indexer\Product\Flat\State as FlatState; +use Magento\Store\Model\StoreManagerInterface; +use Magento\Catalog\Api\Data\ProductInterface; + +/** + * Custom Flat Attribute Test + * + * @SuppressWarnings(PHPMD.CouplingBetweenObjects) + */ +class CustomFlatAttributeTest extends IndexerTestCase +{ + /** + * @var Processor + */ + private $processor; + + /** + * @var \Magento\Framework\ObjectManagerInterface + */ + private $objectManager; + + /** + * @var ProductRepositoryInterface + */ + private $productRepository; + + /** + * @var ProductAttributeRepositoryInterface + */ + private $attributeRepository; + + /** + * @var FlatResource + */ + private $flatResource; + + /** + * @var FlatState + */ + private $flatState; + + /** + * @var StoreManagerInterface + */ + private $storeManager; + + /** + * @var \Magento\Store\Api\Data\StoreInterface + */ + private $savedCurrentStore; + + /** + * @inheritdoc + */ + protected function setUp() + { + $this->objectManager = Bootstrap::getObjectManager(); + $this->processor = $this->objectManager->get(Processor::class); + $this->productRepository = $this->objectManager->get(ProductRepositoryInterface::class); + $this->attributeRepository = $this->objectManager->get(ProductAttributeRepositoryInterface::class); + $this->flatResource = $this->objectManager->get(FlatResource::class); + $this->flatState = $this->objectManager->create(FlatState::class, [ + 'isAvailable' => true + ]); + $this->storeManager = $this->objectManager->get(StoreManagerInterface::class); + $this->savedCurrentStore = $this->storeManager->getStore(); + } + + /** + * @inheritdoc + */ + protected function tearDown() + { + $this->storeManager->setCurrentStore($this->savedCurrentStore); + } + + /** + * Tests that custom product attribute will appear in flat table and can be updated in it. + * + * @magentoDbIsolation disabled + * @magentoAppArea frontend + * @magentoConfigFixture current_store catalog/frontend/flat_catalog_product 1 + * @magentoDataFixture Magento/Catalog/_files/product_simple_with_custom_attribute_in_flat.php + * + * @return void + * @throws \Magento\Framework\Exception\CouldNotSaveException + * @throws \Magento\Framework\Exception\InputException + * @throws \Magento\Framework\Exception\NoSuchEntityException + * @throws \Magento\Framework\Exception\StateException + */ + public function testProductUpdateCustomAttribute() + { + $product = $this->productRepository->get('simple_with_custom_flat_attribute'); + $product->setCustomAttribute('flat_attribute', 'changed flat attribute'); + $this->productRepository->save($product); + + /** @var SearchCriteriaBuilder $searchCriteriaBuilder */ + $searchCriteriaBuilder = $this->objectManager->create(SearchCriteriaBuilder::class); + /** @var \Magento\Framework\Api\SearchCriteriaInterface $searchCriteria */ + $searchCriteria = $searchCriteriaBuilder->addFilter('sku', 'simple_with_custom_flat_attribute') + ->create(); + + $items = $this->productRepository->getList($searchCriteria) + ->getItems(); + $product = reset($items); + $resourceModel = $product->getResourceCollection() + ->getEntity(); + + self::assertInstanceOf( + FlatResource::class, + $resourceModel, + 'Product should be received from flat resource' + ); + + self::assertEquals( + 'changed flat attribute', + $product->getFlatAttribute(), + 'Product flat attribute should be able to change.' + ); + } + + /** + * Tests flat dropdown attribute. + * Tests that flat dropdown attribute will be changed for different flat tables (it means for different stores) + * + * @magentoDbIsolation disabled + * @magentoAppArea adminhtml + * @magentoConfigFixture default_store catalog/frontend/flat_catalog_product 1 + * @magentoConfigFixture default_store catalog/frontend/flat_catalog_category 1 + * @magentoConfigFixture fixturestore_store catalog/frontend/flat_catalog_product 1 + * @magentoConfigFixture fixturestore_store catalog/frontend/flat_catalog_category 1 + * @magentoDataFixture Magento/Catalog/_files/product_simple_multistore.php + * @magentoDataFixture Magento/Catalog/_files/flat_dropdown_attribute.php + * + * @return void + * @throws \Magento\Framework\Exception\CouldNotSaveException + * @throws \Magento\Framework\Exception\InputException + * @throws \Magento\Framework\Exception\NoSuchEntityException + * @throws \Magento\Framework\Exception\StateException + */ + public function testFlatDropDownAttribute() + { + $attribute = $this->attributeRepository->get('flat_attribute'); + $attributeOptions = $attribute->getOptions(); + + $firstStoreAttributeOption = $attributeOptions[1]; + $productStore1 = $this->productRepository->get('simple', false, 1); + $productStore1->setFlatAttribute($firstStoreAttributeOption->getValue()); + $this->productRepository->save($productStore1); + + /** @var StoreRepositoryInterface $storeRepository */ + $storeRepository = $this->objectManager->get(StoreRepositoryInterface::class); + $store = $storeRepository->get('fixturestore'); + + $secondStoreAttributeOption = $attributeOptions[2]; + $productStore2 = $this->productRepository->get('simple', false, $store->getId()); + $productStore2->setFlatAttribute($secondStoreAttributeOption->getValue()); + $this->productRepository->save($productStore2); + + $this->processor = $this->objectManager->create(Processor::class); + $this->processor->reindexAll(); + + $productStore1 = $this->getFlatProductFromStore('simple'); + self::assertEquals($firstStoreAttributeOption->getLabel(), $productStore1->getFlatAttributeValue()); + + $productStore2 = $this->getFlatProductFromStore('simple', $store); + self::assertEquals($secondStoreAttributeOption->getLabel(), $productStore2->getFlatAttributeValue()); + } + + /** + * Get product from store with flat data + * + * @param string $sku + * @param Store|null $store + * @return ProductInterface + */ + private function getFlatProductFromStore(string $sku, $store = null): ProductInterface + { + if ($store) { + $this->storeManager->setCurrentStore($store); + } + + /** @var Collection $productCollection */ + $productCollection = $this->objectManager->create( + Collection::class, + [ + 'catalogProductFlatState' => $this->flatState + ] + ); + + if ($store) { + $productCollection->setStoreId($store->getId()); + } + + $productCollection->setEntity($this->flatResource); + $productCollection->addAttributeToFilter('sku', $sku); + $productCollection->addAttributeToSelect('flat_attribute'); + + return $productCollection->load() + ->getFirstItem(); + } +} diff --git a/dev/tests/integration/testsuite/Magento/Catalog/Model/Indexer/Product/Flat/Action/RowTest.php b/dev/tests/integration/testsuite/Magento/Catalog/Model/Indexer/Product/Flat/Action/RowTest.php index 15c90891878a0..67c81772462d6 100644 --- a/dev/tests/integration/testsuite/Magento/Catalog/Model/Indexer/Product/Flat/Action/RowTest.php +++ b/dev/tests/integration/testsuite/Magento/Catalog/Model/Indexer/Product/Flat/Action/RowTest.php @@ -3,87 +3,110 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ +declare(strict_types=1); + namespace Magento\Catalog\Model\Indexer\Product\Flat\Action; +use Magento\TestFramework\Indexer\TestCase as IndexerTestCase; +use Magento\TestFramework\Helper\Bootstrap; +use Magento\Catalog\Api\ProductRepositoryInterface; +use Magento\Catalog\Model\Indexer\Product\Flat\Processor; +use Magento\Catalog\Block\Product\ListProduct; +use Magento\Catalog\Api\CategoryRepositoryInterface; + /** * Class RowTest */ -class RowTest extends \Magento\TestFramework\Indexer\TestCase +class RowTest extends IndexerTestCase { /** - * @var \Magento\Catalog\Model\Product + * @var Processor */ - protected $_product; + private $processor; /** - * @var \Magento\Catalog\Model\Category + * @var \Magento\Framework\ObjectManagerInterface */ - protected $_category; + private $objectManager; /** - * @var \Magento\Catalog\Model\Indexer\Product\Flat\State + * @var ProductRepositoryInterface */ - protected $_state; + private $productRepository; /** - * @var \Magento\Catalog\Model\Indexer\Product\Flat\Processor + * @var CategoryRepositoryInterface */ - protected $_processor; + private $categoryRepository; + /** + * @inheritdoc + */ protected function setUp() { - $this->_product = \Magento\TestFramework\Helper\Bootstrap::getObjectManager()->create( - \Magento\Catalog\Model\Product::class - ); - $this->_category = \Magento\TestFramework\Helper\Bootstrap::getObjectManager()->create( - \Magento\Catalog\Model\Category::class - ); - $this->_processor = \Magento\TestFramework\Helper\Bootstrap::getObjectManager()->create( - \Magento\Catalog\Model\Indexer\Product\Flat\Processor::class - ); + $this->objectManager = Bootstrap::getObjectManager(); + $this->processor = $this->objectManager->get(Processor::class); + $this->productRepository = $this->objectManager->get(ProductRepositoryInterface::class); + $this->categoryRepository = $this->objectManager->get(CategoryRepositoryInterface::class); } /** + * Tests product update + * * @magentoDbIsolation disabled * @magentoDataFixture Magento/Catalog/_files/row_fixture.php * @magentoConfigFixture current_store catalog/frontend/flat_catalog_product 1 * @magentoAppArea frontend + * + * @return void + * @throws \Magento\Framework\Exception\CouldNotSaveException + * @throws \Magento\Framework\Exception\InputException + * @throws \Magento\Framework\Exception\LocalizedException + * @throws \Magento\Framework\Exception\NoSuchEntityException + * @throws \Magento\Framework\Exception\StateException */ public function testProductUpdate() { - $categoryFactory = \Magento\TestFramework\Helper\Bootstrap::getObjectManager() - ->create(\Magento\Catalog\Model\CategoryFactory::class); - $listProduct = \Magento\TestFramework\Helper\Bootstrap::getObjectManager() - ->create(\Magento\Catalog\Block\Product\ListProduct::class); + /** @var ListProduct $listProduct */ + $listProduct = $this->objectManager->create(ListProduct::class); - $this->_processor->getIndexer()->setScheduled(false); - $this->assertFalse( - $this->_processor->getIndexer()->isScheduled(), + $this->processor->getIndexer() + ->setScheduled(false); + $isScheduled = $this->processor->getIndexer() + ->isScheduled(); + self::assertFalse( + $isScheduled, 'Indexer is in scheduled mode when turned to update on save mode' ); - $this->_product->load(1); - $this->_product->setName('Updated Product'); - $this->_product->save(); + $this->processor->reindexAll(); - $this->_processor->reindexAll(); - - $category = $categoryFactory->create()->load(9); + $product = $this->productRepository->get('simple'); + $product->setName('Updated Product'); + $this->productRepository->save($product); + + /** @var \Magento\Catalog\Api\Data\CategoryInterface $category */ + $category = $this->categoryRepository->get(9); + /** @var \Magento\Catalog\Model\Layer $layer */ $layer = $listProduct->getLayer(); $layer->setCurrentCategory($category); /** @var \Magento\Catalog\Model\ResourceModel\Product\Collection $productCollection */ $productCollection = $layer->getProductCollection(); - $this->assertTrue( + self::assertTrue( $productCollection->isEnabledFlat(), 'Product collection is not using flat resource when flat is on' ); - $this->assertEquals(2, $productCollection->count(), 'Product collection items count must be exactly 2'); + self::assertEquals( + 2, + $productCollection->count(), + 'Product collection items count must be exactly 2' + ); foreach ($productCollection as $product) { /** @var $product \Magento\Catalog\Model\Product */ - if ($product->getId() == 1) { - $this->assertEquals( + if ($product->getSku() === 'simple') { + self::assertEquals( 'Updated Product', $product->getName(), 'Product name from flat does not match with updated name' diff --git a/dev/tests/integration/testsuite/Magento/Catalog/Model/ProductExternalTest.php b/dev/tests/integration/testsuite/Magento/Catalog/Model/ProductExternalTest.php index 8370e514dc2f2..a1923e63972ee 100644 --- a/dev/tests/integration/testsuite/Magento/Catalog/Model/ProductExternalTest.php +++ b/dev/tests/integration/testsuite/Magento/Catalog/Model/ProductExternalTest.php @@ -69,7 +69,7 @@ public function testGetCategoryId() { $this->assertFalse($this->_model->getCategoryId()); $category = new \Magento\Framework\DataObject(['id' => 5]); - + $this->_model->setCategoryIds([5]); $this->objectManager->get(\Magento\Framework\Registry::class)->register('current_category', $category); try { $this->assertEquals(5, $this->_model->getCategoryId()); @@ -83,6 +83,7 @@ public function testGetCategoryId() public function testGetCategory() { $this->assertEmpty($this->_model->getCategory()); + $this->_model->setCategoryIds([3]); $this->objectManager->get(\Magento\Framework\Registry::class) ->register('current_category', new \Magento\Framework\DataObject(['id' => 3])); diff --git a/dev/tests/integration/testsuite/Magento/Catalog/Model/RemoveRedundantImageTest.php b/dev/tests/integration/testsuite/Magento/Catalog/Model/RemoveRedundantImageTest.php new file mode 100644 index 0000000000000..1bc545a25ac32 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Catalog/Model/RemoveRedundantImageTest.php @@ -0,0 +1,82 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Catalog\Model; + +use Magento\Framework\App\Filesystem\DirectoryList; +use Magento\Framework\Filesystem; +use Magento\Framework\Filesystem\Directory\WriteInterface; +use Magento\Framework\ObjectManagerInterface; + +/** + * Test removing old Category Image file from pub/media/catalog/category directory if such Image is not used anymore. + */ +class RemoveRedundantImageTest extends \PHPUnit\Framework\TestCase +{ + /** + * @var ObjectManagerInterface + */ + private $objectManager; + + /** + * @var Filesystem + */ + private $filesystem; + + /** + * @var WriteInterface + */ + private $mediaDirectory; + + /** + * @var CategoryRepository + */ + private $categoryRepository; + + protected function setUp() + { + $this->objectManager = \Magento\TestFramework\Helper\Bootstrap::getObjectManager(); + /** @var Filesystem $filesystem */ + $this->filesystem = $this->objectManager->get(Filesystem::class); + $this->mediaDirectory = $this->filesystem->getDirectoryWrite(DirectoryList::MEDIA); + $this->categoryRepository = $this->objectManager->get(CategoryRepository::class); + } + + /** + * Tests removing Image file if it is not used anymore. + * + * @magentoDataFixture Magento/Catalog/_files/categories_with_image.php + * + * @magentoAppIsolation enabled + * @magentoDbIsolation enabled + */ + public function testRemoveRedundantImage() + { + $imagesPath = 'catalog' . DIRECTORY_SEPARATOR . 'category'; + $absoluteImagesPath = $this->mediaDirectory->getAbsolutePath($imagesPath); + $filePath1 = $absoluteImagesPath . DIRECTORY_SEPARATOR . 'test_image_1.jpg'; + $filePath2 = $absoluteImagesPath . DIRECTORY_SEPARATOR . 'test_image_2.jpg'; + $this->mediaDirectory->create($absoluteImagesPath); + $this->mediaDirectory->touch($filePath1); + $this->mediaDirectory->touch($filePath2); + + $category1 = $this->categoryRepository->get(3); + $category1->setImage('test_image_3.jpg'); + $this->categoryRepository->save($category1); + $category2 = $this->categoryRepository->get(5); + $category2->setImage('test_image_3.jpg'); + $this->categoryRepository->save($category2); + + $this->assertTrue($this->mediaDirectory->isExist($filePath1)); + $this->assertFalse($this->mediaDirectory->isExist($filePath2)); + } + + protected function tearDown() + { + $this->mediaDirectory->delete(); + } +} diff --git a/dev/tests/integration/testsuite/Magento/Catalog/_files/categories_with_image.php b/dev/tests/integration/testsuite/Magento/Catalog/_files/categories_with_image.php new file mode 100644 index 0000000000000..6fc0f4e8b6efa --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Catalog/_files/categories_with_image.php @@ -0,0 +1,53 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +$objectManager = \Magento\TestFramework\Helper\Bootstrap::getObjectManager(); + +/** + * After installation system has two categories: root one with ID:1 and Default category with ID:2 + */ +/** @var $category \Magento\Catalog\Model\Category */ +$category = $objectManager->create(\Magento\Catalog\Model\Category::class); +$category->isObjectNew(true); +$category->setId(3) + ->setName('Category 1') + ->setParentId(2) + ->setPath('1/2/3') + ->setLevel(2) + ->setAvailableSortBy('name') + ->setDefaultSortBy('name') + ->setIsActive(true) + ->setPosition(1) + ->setImage('test_image_1.jpg') + ->save(); + +$category = $objectManager->create(\Magento\Catalog\Model\Category::class); +$category->isObjectNew(true); +$category->setId(4) + ->setName('Category 1.1') + ->setParentId(3) + ->setPath('1/2/3/4') + ->setLevel(3) + ->setAvailableSortBy('name') + ->setDefaultSortBy('name') + ->setIsActive(true) + ->setIsAnchor(true) + ->setPosition(1) + ->setImage('test_image_1.jpg') + ->save(); + +$category = $objectManager->create(\Magento\Catalog\Model\Category::class); +$category->isObjectNew(true); +$category->setId(5) + ->setName('Category 2') + ->setParentId(2) + ->setPath('1/2/5') + ->setLevel(2) + ->setAvailableSortBy('name') + ->setDefaultSortBy('name') + ->setIsActive(true) + ->setPosition(2) + ->setImage('test_image_2.jpg') + ->save(); diff --git a/dev/tests/integration/testsuite/Magento/Catalog/_files/categories_with_image_rollback.php b/dev/tests/integration/testsuite/Magento/Catalog/_files/categories_with_image_rollback.php new file mode 100644 index 0000000000000..d290a164f639e --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Catalog/_files/categories_with_image_rollback.php @@ -0,0 +1,23 @@ +<?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 = $objectManager->get(\Magento\Framework\Registry::class); + +$registry->unregister('isSecureArea'); +$registry->register('isSecureArea', true); + +//Remove categories +/** @var Magento\Catalog\Model\ResourceModel\Category\Collection $collection */ +$collection = $objectManager->create(\Magento\Catalog\Model\ResourceModel\Category\Collection::class); +$collection + ->addAttributeToFilter('level', 2) + ->load() + ->delete(); + +$registry->unregister('isSecureArea'); +$registry->register('isSecureArea', false); diff --git a/dev/tests/integration/testsuite/Magento/Catalog/_files/category_product.php b/dev/tests/integration/testsuite/Magento/Catalog/_files/category_product.php index 6803d96f0c0de..fe61b3e197ca0 100644 --- a/dev/tests/integration/testsuite/Magento/Catalog/_files/category_product.php +++ b/dev/tests/integration/testsuite/Magento/Catalog/_files/category_product.php @@ -15,7 +15,7 @@ )->setParentId( 2 )->setPath( - '1/2/3' + '1/2/333' )->setLevel( 2 )->setAvailableSortBy( diff --git a/dev/tests/integration/testsuite/Magento/Catalog/_files/flat_dropdown_attribute.php b/dev/tests/integration/testsuite/Magento/Catalog/_files/flat_dropdown_attribute.php new file mode 100644 index 0000000000000..290958b56b886 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Catalog/_files/flat_dropdown_attribute.php @@ -0,0 +1,72 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +use Magento\TestFramework\Helper\Bootstrap; +use Magento\Catalog\Setup\CategorySetup; +use Magento\Eav\Model\Entity; +use Magento\Catalog\Model\Product; +use Magento\Catalog\Api\Data\ProductAttributeInterface; +use Magento\Eav\Api\Data\AttributeOptionInterfaceFactory; +use Magento\Catalog\Api\ProductAttributeRepositoryInterface; + +/** @var \Magento\TestFramework\ObjectManager $objectManager */ +$objectManager = Bootstrap::getObjectManager(); +/** @var AttributeOptionInterfaceFactory $attributeOptionFactory */ +$attributeOptionFactory = $objectManager->get(AttributeOptionInterfaceFactory::class); +/** @var ProductAttributeRepositoryInterface $attributeRepository */ +$attributeRepository = $objectManager->get(ProductAttributeRepositoryInterface::class); +/** @var $installer CategorySetup */ +$installer = $objectManager->create(CategorySetup::class); +$entityModel = $objectManager->create(Entity::class); +$attributeSetId = $installer->getAttributeSetId(Product::ENTITY, 'Default'); +$entityTypeId = $entityModel->setType(Product::ENTITY) + ->getTypeId(); +$groupId = $installer->getDefaultAttributeGroupId($entityTypeId, $attributeSetId); +/** @var ProductAttributeInterface $attribute */ +$attribute = $objectManager->create(ProductAttributeInterface::class); + +$attribute->setAttributeCode('flat_attribute') + ->setEntityTypeId($entityTypeId) + ->setIsVisible(true) + ->setFrontendInput('select') + ->setIsFilterable(1) + ->setIsUserDefined(1) + ->setUsedInProductListing(1) + ->setBackendType('int') + ->setIsUsedInGrid(1) + ->setIsVisibleInGrid(1) + ->setIsFilterable(0) + ->setIsHtmlAllowedOnFront(1) + ->setIsFilterableInGrid(1) + ->setAttributeGroupId($groupId) + ->setIsGlobal(0) + ->setIsUsedInProductListing(1) + ->setFrontendLabel('nobody cares') + ->setAttributeGroupId($groupId) + ->setAttributeSetId(4); + +$optionsArray = [ + [ + 'label' => 'Option 1', + 'value' => 'option_1' + ], + [ + 'label' => 'Option 2', + 'value' => 'option_2' + ], + [ + 'label' => 'Option 3', + 'value' => 'option_3' + ], +]; + +$options = []; +foreach ($optionsArray as $option) { + $options[] = $attributeOptionFactory->create(['data' => $option]); +} +$attribute->setOptions($options); +$attributeRepository->save($attribute); diff --git a/dev/tests/integration/testsuite/Magento/Catalog/_files/flat_dropdown_attribute_rollback.php b/dev/tests/integration/testsuite/Magento/Catalog/_files/flat_dropdown_attribute_rollback.php new file mode 100644 index 0000000000000..d56fefaa99aa2 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Catalog/_files/flat_dropdown_attribute_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\TestFramework\Helper\Bootstrap; +use Magento\Catalog\Api\ProductAttributeRepositoryInterface; +use Magento\Framework\Exception\NoSuchEntityException; + +/** @var \Magento\TestFramework\ObjectManager $objectManager */ +$objectManager = Bootstrap::getObjectManager(); +/** @var ProductAttributeRepositoryInterface $attributeRepository */ +$attributeRepository = $objectManager->get(ProductAttributeRepositoryInterface::class); + +try { + $attribute = $attributeRepository->get('flat_attribute'); + $attributeRepository->delete($attribute); +} catch (NoSuchEntityException $e) { +} diff --git a/dev/tests/integration/testsuite/Magento/Catalog/_files/product_simple_with_custom_attribute_in_flat.php b/dev/tests/integration/testsuite/Magento/Catalog/_files/product_simple_with_custom_attribute_in_flat.php new file mode 100644 index 0000000000000..2b1b271a8bb3c --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Catalog/_files/product_simple_with_custom_attribute_in_flat.php @@ -0,0 +1,79 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +use Magento\TestFramework\Helper\Bootstrap; +use Magento\Catalog\Api\ProductRepositoryInterface; +use Magento\Catalog\Model\ProductFactory; +use Magento\Catalog\Api\Data\ProductAttributeInterface; +use Magento\Catalog\Api\ProductAttributeRepositoryInterface; +use Magento\Catalog\Model\Indexer\Product\Flat\Processor; +use Magento\Catalog\Setup\CategorySetup; +use Magento\Eav\Model\Entity; +use Magento\Catalog\Model\Product; +use Magento\Catalog\Model\Product\Type; + +/** @var \Magento\TestFramework\ObjectManager $objectManager */ +$objectManager = Bootstrap::getObjectManager(); +/** @var ProductRepositoryInterface $productRepository */ +$productRepository = $objectManager->get(ProductRepositoryInterface::class); +/** @var ProductFactory $productFactory */ +$productFactory = $objectManager->get(ProductFactory::class); +/** @var ProductAttributeRepositoryInterface $attributeRepository */ +$attributeRepository = $objectManager->get(ProductAttributeRepositoryInterface::class); + + +/** @var $installer CategorySetup */ +$installer = $objectManager->create(CategorySetup::class); +$entityModel = $objectManager->create(Entity::class); +$attributeSetId = $installer->getAttributeSetId(Product::ENTITY, 'Default'); +$entityTypeId = $entityModel->setType(Product::ENTITY) + ->getTypeId(); +$groupId = $installer->getDefaultAttributeGroupId($entityTypeId, $attributeSetId); + +/** @var ProductAttributeInterface $attribute */ +$attribute = $objectManager->create(ProductAttributeInterface::class); + +$attribute->setAttributeCode('flat_attribute') + ->setEntityTypeId($entityTypeId) + ->setIsVisible(true) + ->setFrontendInput('text') + ->setIsFilterable(1) + ->setIsUserDefined(1) + ->setUsedInProductListing(1) + ->setBackendType('varchar') + ->setIsUsedInGrid(1) + ->setIsVisibleInGrid(1) + ->setIsFilterableInGrid(1) + ->setFrontendLabel('nobody cares') + ->setAttributeGroupId($groupId) + ->setAttributeSetId(4); + +$attributeRepository->save($attribute); + +/** @var Processor $processor */ +$processor = $objectManager->create(Processor::class); +$scheduled = $processor->getIndexer() + ->isScheduled(); +$processor->reindexAll(); + +$product = $productFactory->create() + ->setTypeId(Type::TYPE_SIMPLE) + ->setAttributeSetId(4) + ->setName('Simple With Attribute That Used In Flat') + ->setSku('simple_with_custom_flat_attribute') + ->setPrice(100) + ->setVisibility(1) + ->setStockData( + [ + 'use_config_manage_stock' => 1, + 'qty' => 100, + 'is_in_stock' => 1, + ] + ) + ->setStatus(1); +$product->setCustomAttribute('flat_attribute', 'flat attribute value'); +$productRepository->save($product); diff --git a/dev/tests/integration/testsuite/Magento/Catalog/_files/product_simple_with_custom_attribute_in_flat_rollback.php b/dev/tests/integration/testsuite/Magento/Catalog/_files/product_simple_with_custom_attribute_in_flat_rollback.php new file mode 100644 index 0000000000000..c1892d504ecc3 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Catalog/_files/product_simple_with_custom_attribute_in_flat_rollback.php @@ -0,0 +1,41 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +use Magento\Framework\Registry; +use Magento\TestFramework\Helper\Bootstrap; +use Magento\Catalog\Api\ProductRepositoryInterface; +use Magento\Framework\Exception\NoSuchEntityException; +use Magento\Catalog\Api\ProductAttributeRepositoryInterface; + +/** @var Magento\Framework\ObjectManagerInterface $objectManager */ +$objectManager = Bootstrap::getObjectManager(); +/** @var ProductRepositoryInterface $productRepository */ +$productRepository = $objectManager->get(ProductRepositoryInterface::class); +/** @var ProductAttributeRepositoryInterface $attributeRepository */ +$attributeRepository = $objectManager->get(ProductAttributeRepositoryInterface::class); +/** @var Registry $registry */ +$registry = $objectManager->get(Registry::class); + +$registry->unregister('isSecureArea'); +$registry->register('isSecureArea', true); + +try { + /** @var \Magento\Catalog\Api\Data\ProductInterface $product */ + $product = $productRepository->get('simple_with_custom_flat_attribute'); + $productRepository->delete($product); +} catch (NoSuchEntityException $e) { +} + +try { + /** @var \Magento\Catalog\Api\Data\ProductAttributeInterface $attribute */ + $attribute = $attributeRepository->get('flat_attribute'); + $attributeRepository->delete($attribute); +} catch (NoSuchEntityException $e) { +} + +$registry->unregister('isSecureArea'); +$registry->register('isSecureArea', false); diff --git a/dev/tests/integration/testsuite/Magento/Catalog/_files/products_in_different_stores.php b/dev/tests/integration/testsuite/Magento/Catalog/_files/products_in_different_stores.php new file mode 100644 index 0000000000000..6102738bd6be4 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Catalog/_files/products_in_different_stores.php @@ -0,0 +1,122 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +use Magento\TestFramework\Helper\Bootstrap; +use Magento\Framework\Indexer\IndexerRegistry; +use Magento\Catalog\Model\Product; +use Magento\Catalog\Model\Product\Type; +use Magento\Catalog\Model\Product\Visibility; +use Magento\Catalog\Model\Product\Attribute\Source\Status; +use Magento\Catalog\Api\ProductRepositoryInterface; + +require __DIR__ . '/../../Store/_files/core_fixturestore.php'; + +$objectManager = Bootstrap::getObjectManager(); + +/** @var Magento\Store\Model\Website $website */ +$website = $objectManager->get(Magento\Store\Model\Website::class); + +$website->setData( + [ + 'code' => 'second_website', + 'name' => 'Test Website', + ] +); + +$website->save(); + +$objectManager->get(\Magento\Store\Model\StoreManagerInterface::class)->reinitStores(); + +/** @var IndexerRegistry $indexerRegistry */ +$indexerRegistry = $objectManager->create(IndexerRegistry::class); +$indexer = $indexerRegistry->get('catalogsearch_fulltext'); + +$indexer->reindexAll(); + +$category = $objectManager->create(\Magento\Catalog\Model\Category::class); +$category->isObjectNew(true); +$category->setId(96377) + ->setName('Category 1') + ->setParentId(2) + ->setPath('1/2/96377') + ->setLevel(2) + ->setAvailableSortBy('name') + ->setDefaultSortBy('name') + ->setIsActive(true) + ->setPosition(1) + ->save(); + +/** @var $product Product */ +$product = $objectManager->create(Product::class); +$product->isObjectNew(true); +$product->setTypeId(Type::TYPE_SIMPLE) + ->setAttributeSetId(4) + ->setWebsiteIds([1]) + ->setName('Simple Product') + ->setSku('simple_1') + ->setPrice(10) + ->setWeight(1) + ->setShortDescription("Short description") + ->setTaxClassId(0) + ->setDescription('Description with <b>html tag</b>') + ->setMetaTitle('meta title') + ->setMetaKeyword('meta keyword') + ->setMetaDescription('meta description') + ->setVisibility(Visibility::VISIBILITY_BOTH) + ->setStatus(Status::STATUS_ENABLED) + ->setCategoryIds([96377]); + +/** @var ProductRepositoryInterface $productRepository */ +$productRepository = $objectManager->create(ProductRepositoryInterface::class); +$productRepository->save($product); + +/** @var $product Product */ +$product = $objectManager->create(Product::class); +$product->isObjectNew(true); +$product->setTypeId(Type::TYPE_SIMPLE) + ->setAttributeSetId(4) + ->setWebsiteIds([$website->getId()]) + ->setName('Simple Product 2') + ->setSku('simple_2') + ->setPrice(10) + ->setWeight(1) + ->setShortDescription("Short description") + ->setTaxClassId(0) + ->setDescription('Description with <b>html tag</b>') + ->setMetaTitle('meta title') + ->setMetaKeyword('meta keyword') + ->setMetaDescription('meta description') + ->setVisibility(Visibility::VISIBILITY_BOTH) + ->setStatus(Status::STATUS_ENABLED) + ->setCategoryIds([96377]); + +/** @var ProductRepositoryInterface $productRepository */ +$productRepository = $objectManager->create(ProductRepositoryInterface::class); +$productRepository->save($product); + +/** @var $product Product */ +$product = $objectManager->create(Product::class); +$product->isObjectNew(true); +$product->setTypeId(Type::TYPE_SIMPLE) + ->setAttributeSetId(4) + ->setWebsiteIds([1, $website->getId()]) + ->setName('Simple Product 3') + ->setSku('simple_3') + ->setPrice(10) + ->setWeight(1) + ->setShortDescription("Short description") + ->setTaxClassId(0) + ->setDescription('Description with <b>html tag</b>') + ->setMetaTitle('meta title') + ->setMetaKeyword('meta keyword') + ->setMetaDescription('meta description') + ->setVisibility(Visibility::VISIBILITY_BOTH) + ->setStatus(Status::STATUS_ENABLED) + ->setCategoryIds([96377]); + +/** @var ProductRepositoryInterface $productRepository */ +$productRepository = $objectManager->create(ProductRepositoryInterface::class); +$productRepository->save($product); diff --git a/dev/tests/integration/testsuite/Magento/Catalog/_files/products_in_different_stores_rollback.php b/dev/tests/integration/testsuite/Magento/Catalog/_files/products_in_different_stores_rollback.php new file mode 100644 index 0000000000000..9b957b75eb2a3 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Catalog/_files/products_in_different_stores_rollback.php @@ -0,0 +1,46 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +require __DIR__ . '/../../Store/_files/core_fixturestore_rollback.php'; + +$objectManager = \Magento\TestFramework\Helper\Bootstrap::getObjectManager(); +/** @var \Magento\Framework\Registry $registry */ +$registry = $objectManager->get(\Magento\Framework\Registry::class); + +$registry->unregister('isSecureArea'); +$registry->register('isSecureArea', true); + +//Remove category +/** @var $category \Magento\Catalog\Model\Category */ +$category = $objectManager->create(\Magento\Catalog\Model\Category::class); +$category->load(96377); +if ($category->getId()) { + $category->delete(); +} + +$productSkuList = ['simple_1', 'simple_2', 'simple_3']; +foreach ($productSkuList as $sku) { + try { + $productRepository = \Magento\TestFramework\Helper\Bootstrap::getObjectManager() + ->get(\Magento\Catalog\Api\ProductRepositoryInterface::class); + $product = $productRepository->get($sku, true); + if ($product->getId()) { + $productRepository->delete($product); + } + } catch (\Magento\Framework\Exception\NoSuchEntityException $e) { + //Product already removed + } +} + +/** @var Magento\Store\Model\Website $website */ +$website = $objectManager->get(Magento\Store\Model\Website::class); +$website->load('second_website', 'code'); +if ($website->getId()) { + $website->delete(); +} + +$registry->unregister('isSecureArea'); +$registry->register('isSecureArea', false); diff --git a/dev/tests/integration/testsuite/Magento/CatalogImportExport/Model/Import/ProductTest.php b/dev/tests/integration/testsuite/Magento/CatalogImportExport/Model/Import/ProductTest.php index f3cb01a9bbe2e..9cc7c452e646f 100644 --- a/dev/tests/integration/testsuite/Magento/CatalogImportExport/Model/Import/ProductTest.php +++ b/dev/tests/integration/testsuite/Magento/CatalogImportExport/Model/Import/ProductTest.php @@ -25,6 +25,7 @@ use Magento\Framework\Filesystem; use Magento\Framework\Registry; use Magento\ImportExport\Model\Import; +use Magento\ImportExport\Model\Import\ErrorProcessing\ProcessingErrorAggregatorInterface; use Magento\Store\Model\Store; use Psr\Log\LoggerInterface; @@ -36,6 +37,7 @@ * @magentoDataFixtureBeforeTransaction Magento/Catalog/_files/enable_catalog_product_reindex_schedule.php * @SuppressWarnings(PHPMD.CouplingBetweenObjects) * @SuppressWarnings(PHPMD.ExcessiveClassComplexity) + * @SuppressWarnings(PHPMD.ExcessivePublicCount) */ class ProductTest extends \Magento\TestFramework\Indexer\TestCase { @@ -69,16 +71,20 @@ class ProductTest extends \Magento\TestFramework\Indexer\TestCase */ private $logger; + /** + * @inheritdoc + */ protected function setUp() { $this->objectManager = \Magento\TestFramework\Helper\Bootstrap::getObjectManager(); $this->logger = $this->getMockBuilder(LoggerInterface::class) ->disableOriginalConstructor() ->getMock(); - $this->_model = \Magento\TestFramework\Helper\Bootstrap::getObjectManager()->create( + $this->_model = $this->objectManager->create( \Magento\CatalogImportExport\Model\Import\Product::class, ['logger' => $this->logger] ); + parent::setUp(); } @@ -103,7 +109,7 @@ protected function setUp() protected $_assertOptionValues = [ 'title' => 'option_title', 'price' => 'price', - 'sku' => 'sku' + 'sku' => 'sku', ]; /** @@ -127,24 +133,19 @@ protected function setUp() public function testSaveProductsVisibility() { /** @var \Magento\Catalog\Api\ProductRepositoryInterface $productRepository */ - $productRepository = \Magento\TestFramework\Helper\Bootstrap::getObjectManager()->create( - \Magento\Catalog\Api\ProductRepositoryInterface::class - ); + $productRepository = $this->objectManager->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]; $productsBeforeImport = []; foreach ($existingProductIds as $productId) { - $product = \Magento\TestFramework\Helper\Bootstrap::getObjectManager()->create( - \Magento\Catalog\Model\Product::class - ); + $product = $this->objectManager->create(\Magento\Catalog\Model\Product::class); $product->load($productId); $productsBeforeImport[] = $product; } - $filesystem = \Magento\TestFramework\Helper\Bootstrap::getObjectManager() - ->create(\Magento\Framework\Filesystem::class); + $filesystem = $this->objectManager->create(\Magento\Framework\Filesystem::class); $directory = $filesystem->getDirectoryWrite(DirectoryList::ROOT); $source = $this->objectManager->create( @@ -167,9 +168,7 @@ public function testSaveProductsVisibility() /** @var $productBeforeImport \Magento\Catalog\Model\Product */ foreach ($productsBeforeImport as $productBeforeImport) { /** @var $productAfterImport \Magento\Catalog\Model\Product */ - $productAfterImport = \Magento\TestFramework\Helper\Bootstrap::getObjectManager()->create( - \Magento\Catalog\Model\Product::class - ); + $productAfterImport = $this->objectManager->create(\Magento\Catalog\Model\Product::class); $productAfterImport->load($productBeforeImport->getId()); $this->assertEquals($productBeforeImport->getVisibility(), $productAfterImport->getVisibility()); @@ -188,9 +187,7 @@ public function testSaveProductsVisibility() public function testSaveStockItemQty() { /** @var \Magento\Catalog\Api\ProductRepositoryInterface $productRepository */ - $productRepository = \Magento\TestFramework\Helper\Bootstrap::getObjectManager()->create( - \Magento\Catalog\Api\ProductRepositoryInterface::class - ); + $productRepository = $this->objectManager->create(\Magento\Catalog\Api\ProductRepositoryInterface::class); $id1 = $productRepository->get('simple1')->getId(); $id2 = $productRepository->get('simple2')->getId(); $id3 = $productRepository->get('simple3')->getId(); @@ -198,16 +195,13 @@ public function testSaveStockItemQty() $stockItems = []; foreach ($existingProductIds as $productId) { /** @var $stockRegistry \Magento\CatalogInventory\Model\StockRegistry */ - $stockRegistry = \Magento\TestFramework\Helper\Bootstrap::getObjectManager()->create( - \Magento\CatalogInventory\Model\StockRegistry::class - ); + $stockRegistry = $this->objectManager->create(\Magento\CatalogInventory\Model\StockRegistry::class); $stockItem = $stockRegistry->getStockItem($productId, 1); $stockItems[$productId] = $stockItem; } - $filesystem = \Magento\TestFramework\Helper\Bootstrap::getObjectManager() - ->create(\Magento\Framework\Filesystem::class); + $filesystem = $this->objectManager->create(\Magento\Framework\Filesystem::class); $directory = $filesystem->getDirectoryWrite(DirectoryList::ROOT); $source = $this->objectManager->create( \Magento\ImportExport\Model\Import\Source\Csv::class, @@ -229,9 +223,7 @@ public function testSaveStockItemQty() /** @var $stockItmBeforeImport \Magento\CatalogInventory\Model\Stock\Item */ foreach ($stockItems as $productId => $stockItmBeforeImport) { /** @var $stockRegistry \Magento\CatalogInventory\Model\StockRegistry */ - $stockRegistry = \Magento\TestFramework\Helper\Bootstrap::getObjectManager()->create( - \Magento\CatalogInventory\Model\StockRegistry::class - ); + $stockRegistry = $this->objectManager->create(\Magento\CatalogInventory\Model\StockRegistry::class); $stockItemAfterImport = $stockRegistry->getStockItem($productId, 1); @@ -251,8 +243,7 @@ public function testSaveStockItemQty() */ public function testStockState() { - $filesystem = \Magento\TestFramework\Helper\Bootstrap::getObjectManager() - ->create(\Magento\Framework\Filesystem::class); + $filesystem = $this->objectManager->create(\Magento\Framework\Filesystem::class); $directory = $filesystem->getDirectoryWrite(DirectoryList::ROOT); $source = $this->objectManager->create( \Magento\ImportExport\Model\Import\Source\Csv::class, @@ -292,7 +283,7 @@ public function testSaveCustomOptions($importFile, $sku, $expectedOptionsQty) $importModel->importData(); /** @var \Magento\Catalog\Api\ProductRepositoryInterface $productRepository */ - $productRepository = \Magento\TestFramework\Helper\Bootstrap::getObjectManager()->create( + $productRepository = $this->objectManager->create( \Magento\Catalog\Api\ProductRepositoryInterface::class ); $product = $productRepository->get($sku); @@ -350,13 +341,12 @@ public function testSaveCustomOptions($importFile, $sku, $expectedOptionsQty) */ public function testSaveCustomOptionsWithMultipleStoreViews() { - $objectManager = \Magento\TestFramework\Helper\Bootstrap::getObjectManager(); /** @var \Magento\Store\Model\StoreManagerInterface $storeManager */ - $storeManager = $objectManager->get(\Magento\Store\Model\StoreManagerInterface::class); + $storeManager = $this->objectManager->get(\Magento\Store\Model\StoreManagerInterface::class); $storeCodes = [ 'admin', 'default', - 'fixture_second_store' + 'fixture_second_store', ]; /** @var \Magento\Store\Model\StoreManagerInterface $storeManager */ $importFile = 'product_with_custom_options_and_multiple_store_views.csv'; @@ -367,9 +357,7 @@ public function testSaveCustomOptionsWithMultipleStoreViews() $this->assertTrue($errors->getErrorsCount() == 0); $importModel->importData(); /** @var \Magento\Catalog\Api\ProductRepositoryInterface $productRepository */ - $productRepository = \Magento\TestFramework\Helper\Bootstrap::getObjectManager()->create( - \Magento\Catalog\Api\ProductRepositoryInterface::class - ); + $productRepository = $this->objectManager->create(\Magento\Catalog\Api\ProductRepositoryInterface::class); foreach ($storeCodes as $storeCode) { $storeManager->setCurrentStore($storeCode); $product = $productRepository->get($sku); @@ -416,24 +404,24 @@ public function testSaveCustomOptionsWithMultipleStoreViews() /** * @return array */ - public function getBehaviorDataProvider() + public function getBehaviorDataProvider(): array { return [ 'Append behavior with existing product' => [ 'importFile' => 'product_with_custom_options.csv', 'sku' => 'simple', - 'expectedOptionsQty' => 6 + 'expectedOptionsQty' => 6, ], 'Append behavior with existing product and without options in import file' => [ 'importFile' => 'product_without_custom_options.csv', 'sku' => 'simple', - 'expectedOptionsQty' => 0 + 'expectedOptionsQty' => 0, ], 'Append behavior with new product' => [ 'importFile' => 'product_with_custom_options_new.csv', 'sku' => 'simple_new', - 'expectedOptionsQty' => 4 - ] + 'expectedOptionsQty' => 4, + ], ]; } @@ -444,8 +432,7 @@ public function getBehaviorDataProvider() */ private function createImportModel($pathToFile, $behavior = \Magento\ImportExport\Model\Import::BEHAVIOR_APPEND) { - $filesystem = \Magento\TestFramework\Helper\Bootstrap::getObjectManager() - ->create(\Magento\Framework\Filesystem::class); + $filesystem = $this->objectManager->create(\Magento\Framework\Filesystem::class); $directory = $filesystem->getDirectoryWrite(DirectoryList::ROOT); /** @var \Magento\ImportExport\Model\Import\Source\Csv $source */ @@ -458,9 +445,7 @@ private function createImportModel($pathToFile, $behavior = \Magento\ImportExpor ); /** @var \Magento\CatalogImportExport\Model\Import\Product $importModel */ - $importModel = \Magento\TestFramework\Helper\Bootstrap::getObjectManager()->create( - \Magento\CatalogImportExport\Model\Import\Product::class - ); + $importModel = $this->objectManager->create(\Magento\CatalogImportExport\Model\Import\Product::class); $importModel->setParameters(['behavior' => $behavior, 'entity' => 'catalog_product'])->setSource($source); return $importModel; @@ -497,24 +482,19 @@ private function getCustomOptionValues($productSku) public function testSaveDatetimeAttribute() { /** @var \Magento\Catalog\Api\ProductRepositoryInterface $productRepository */ - $productRepository = \Magento\TestFramework\Helper\Bootstrap::getObjectManager()->create( - \Magento\Catalog\Api\ProductRepositoryInterface::class - ); + $productRepository = $this->objectManager->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]; $productsBeforeImport = []; foreach ($existingProductIds as $productId) { - $product = \Magento\TestFramework\Helper\Bootstrap::getObjectManager()->create( - \Magento\Catalog\Model\Product::class - ); + $product = $this->objectManager->create(\Magento\Catalog\Model\Product::class); $product->load($productId); $productsBeforeImport[$product->getSku()] = $product; } - $filesystem = \Magento\TestFramework\Helper\Bootstrap::getObjectManager() - ->create(\Magento\Framework\Filesystem::class); + $filesystem = $this->objectManager->create(\Magento\Framework\Filesystem::class); $directory = $filesystem->getDirectoryWrite(DirectoryList::ROOT); $source = $this->objectManager->create( @@ -540,9 +520,7 @@ public function testSaveDatetimeAttribute() $productBeforeImport = $productsBeforeImport[$row['sku']]; /** @var $productAfterImport \Magento\Catalog\Model\Product */ - $productAfterImport = \Magento\TestFramework\Helper\Bootstrap::getObjectManager()->create( - \Magento\Catalog\Model\Product::class - ); + $productAfterImport = $this->objectManager->create(\Magento\Catalog\Model\Product::class); $productAfterImport->load($productBeforeImport->getId()); $this->assertEquals( @strtotime(date('m/d/Y', @strtotime($row['news_from_date']))), @@ -588,7 +566,7 @@ protected function getExpectedOptionsData($pathToFile, $storeCode = '') $option = array_values( array_map( function ($input) { - $data = explode('=',$input); + $data = explode('=', $input); return [$data[0] => $data[1]]; }, explode(',', $optionData) @@ -624,11 +602,12 @@ function ($input) { } } } + return [ 'id' => $expectedOptionId, 'options' => $expectedOptions, 'data' => $expectedData, - 'values' => $expectedValues + 'values' => $expectedValues, ]; } @@ -662,13 +641,14 @@ protected function mergeWithExistingData( $expectedData[$existingOptionId] = array_merge( $this->getOptionData($option), $expectedData[$existingOptionId] - ); if ($optionValues) { - foreach ($optionValues as $optionKey => $optionValue) - $expectedValues[$existingOptionId][$optionKey] = array_merge( - $optionValue, $expectedValues[$existingOptionId][$optionKey] - ); + foreach ($optionValues as $optionKey => $optionValue) { + $expectedValues[$existingOptionId][$optionKey] = array_merge( + $optionValue, + $expectedValues[$existingOptionId][$optionKey] + ); + } } } } @@ -761,7 +741,6 @@ protected function getOptionValues(\Magento\Catalog\Model\Product\Option $option /** * Test that product import with images works properly * - * @magentoDataIsolation enabled * @magentoDataFixture mediaImportImageFixture * @magentoAppIsolation enabled * @SuppressWarnings(PHPMD.ExcessiveMethodLength) @@ -812,7 +791,6 @@ public function testSaveMediaImage() /** * Test that errors occurred during importing images are logged. * - * @magentoDataIsolation enabled * @magentoAppIsolation enabled * @magentoDataFixture mediaImportImageFixture * @magentoDataFixture mediaImportImageFixtureError @@ -905,7 +883,6 @@ public static function mediaImportImageFixtureError() } } - /** * Export CSV string to array * @@ -923,7 +900,7 @@ protected function csvToArray($content, $entityId = null) $data['header'] = str_getcsv($line); } else { $row = array_combine($data['header'], str_getcsv($line)); - if (!is_null($entityId) && !empty($row[$entityId])) { + if ($entityId !== null && !empty($row[$entityId])) { $data['data'][$row[$entityId]] = $row; } else { $data['data'][] = $row; @@ -944,9 +921,7 @@ public function testInvalidSkuLink() { // import data from CSV file $pathToFile = __DIR__ . '/_files/products_to_import_invalid_attribute_set.csv'; - $filesystem = \Magento\TestFramework\Helper\Bootstrap::getObjectManager()->create( - \Magento\Framework\Filesystem::class - ); + $filesystem = $this->objectManager->create(\Magento\Framework\Filesystem::class); $directory = $filesystem->getDirectoryWrite(DirectoryList::ROOT); $source = $this->objectManager->create( \Magento\ImportExport\Model\Import\Source\Csv::class, @@ -958,6 +933,7 @@ public function testInvalidSkuLink() $errors = $this->_model->setParameters( [ 'behavior' => \Magento\ImportExport\Model\Import::BEHAVIOR_APPEND, + Import::FIELD_NAME_VALIDATION_STRATEGY => null, 'entity' => 'catalog_product' ] )->setSource( @@ -971,7 +947,7 @@ public function testInvalidSkuLink() ); $this->_model->importData(); - $productCollection = \Magento\TestFramework\Helper\Bootstrap::getObjectManager()->create( + $productCollection = $this->objectManager->create( \Magento\Catalog\Model\ResourceModel\Product\Collection::class ); @@ -992,9 +968,7 @@ public function testInvalidSkuLink() */ public function testValidateInvalidMultiselectValues() { - $filesystem = \Magento\TestFramework\Helper\Bootstrap::getObjectManager()->create( - \Magento\Framework\Filesystem::class - ); + $filesystem = $this->objectManager->create(\Magento\Framework\Filesystem::class); $directory = $filesystem->getDirectoryWrite(DirectoryList::ROOT); $source = $this->objectManager->create( \Magento\ImportExport\Model\Import\Source\Csv::class, @@ -1028,9 +1002,7 @@ public function testValidateInvalidMultiselectValues() */ public function testProductsWithMultipleStores() { - $objectManager = \Magento\TestFramework\Helper\Bootstrap::getObjectManager(); - - $filesystem = $objectManager->create(\Magento\Framework\Filesystem::class); + $filesystem = $this->objectManager->create(\Magento\Framework\Filesystem::class); $directory = $filesystem->getDirectoryWrite(DirectoryList::ROOT); $source = $this->objectManager->create( \Magento\ImportExport\Model\Import\Source\Csv::class, @@ -1050,15 +1022,15 @@ public function testProductsWithMultipleStores() $this->_model->importData(); /** @var \Magento\Catalog\Model\Product $product */ - $product = $objectManager->create(\Magento\Catalog\Model\Product::class); + $product = $this->objectManager->create(\Magento\Catalog\Model\Product::class); $id = $product->getIdBySku('Configurable 03'); $product->load($id); $this->assertEquals('1', $product->getHasOptions()); - $objectManager->get(\Magento\Store\Model\StoreManagerInterface::class)->setCurrentStore('fixturestore'); + $this->objectManager->get(\Magento\Store\Model\StoreManagerInterface::class)->setCurrentStore('fixturestore'); /** @var \Magento\Catalog\Model\Product $simpleProduct */ - $simpleProduct = $objectManager->create(\Magento\Catalog\Model\Product::class); + $simpleProduct = $this->objectManager->create(\Magento\Catalog\Model\Product::class); $id = $simpleProduct->getIdBySku('Configurable 03-Option 1'); $simpleProduct->load($id); $this->assertTrue(count($simpleProduct->getWebsiteIds()) == 2); @@ -1076,9 +1048,7 @@ public function testProductsWithMultipleStores() */ public function testGenerateUrlsWithMultipleStores() { - $objectManager = \Magento\TestFramework\Helper\Bootstrap::getObjectManager(); - - $filesystem = $objectManager->create(\Magento\Framework\Filesystem::class); + $filesystem = $this->objectManager->create(\Magento\Framework\Filesystem::class); $directory = $filesystem->getDirectoryWrite(DirectoryList::ROOT); $source = $this->objectManager->create( \Magento\ImportExport\Model\Import\Source\Csv::class, @@ -1109,22 +1079,21 @@ public function testGenerateUrlsWithMultipleStores() */ private function assertProductRequestPath($storeCode, $expected) { - $objectManager = \Magento\TestFramework\Helper\Bootstrap::getObjectManager(); /** @var Store $storeCode */ - $store = $objectManager->get(Store::class); + $store = $this->objectManager->get(Store::class); $storeId = $store->load($storeCode)->getId(); /** @var Category $category */ - $category = $objectManager->get(Category::class); + $category = $this->objectManager->get(Category::class); $category->setStoreId($storeId); $category->load(555); /** @var Registry $registry */ - $registry = $objectManager->get(Registry::class); + $registry = $this->objectManager->get(Registry::class); $registry->register('current_category', $category); /** @var \Magento\Catalog\Model\Product $product */ - $product = $objectManager->create(\Magento\Catalog\Model\Product::class); + $product = $this->objectManager->create(\Magento\Catalog\Model\Product::class); $id = $product->getIdBySku('product'); $product->setStoreId($storeId); $product->load($id); @@ -1140,9 +1109,7 @@ public function testProductWithInvalidWeight() { // import data from CSV file $pathToFile = __DIR__ . '/_files/product_to_import_invalid_weight.csv'; - $filesystem = \Magento\TestFramework\Helper\Bootstrap::getObjectManager()->create( - \Magento\Framework\Filesystem::class - ); + $filesystem = $this->objectManager->create(\Magento\Framework\Filesystem::class); $directory = $filesystem->getDirectoryWrite(DirectoryList::ROOT); $source = $this->objectManager->create( @@ -1175,9 +1142,7 @@ public function testProductCategories($fixture, $separator) { // import data from CSV file $pathToFile = __DIR__ . '/_files/' . $fixture; - $filesystem = \Magento\TestFramework\Helper\Bootstrap::getObjectManager()->create( - \Magento\Framework\Filesystem::class - ); + $filesystem = $this->objectManager->create(\Magento\Framework\Filesystem::class); $directory = $filesystem->getDirectoryWrite(DirectoryList::ROOT); $source = $this->objectManager->create( @@ -1200,14 +1165,11 @@ public function testProductCategories($fixture, $separator) $this->assertTrue($errors->getErrorsCount() == 0); $this->_model->importData(); - $objectManager = \Magento\TestFramework\Helper\Bootstrap::getObjectManager(); - $resource = $objectManager->get(\Magento\Catalog\Model\ResourceModel\Product::class); + $resource = $this->objectManager->get(\Magento\Catalog\Model\ResourceModel\Product::class); $productId = $resource->getIdBySku('simple1'); $this->assertTrue(is_numeric($productId)); /** @var \Magento\Catalog\Model\Product $product */ - $product = \Magento\TestFramework\Helper\Bootstrap::getObjectManager()->create( - \Magento\Catalog\Model\Product::class - ); + $product = $this->objectManager->create(\Magento\Catalog\Model\Product::class); $product->load($productId); $this->assertFalse($product->isObjectNew()); $categories = $product->getCategoryIds(); @@ -1240,9 +1202,7 @@ public function testProductPositionInCategory() $category->setPostedProducts($categoryProducts); $category->save(); - $filesystem = \Magento\TestFramework\Helper\Bootstrap::getObjectManager()->create( - \Magento\Framework\Filesystem::class - ); + $filesystem = $this->objectManager->create(\Magento\Framework\Filesystem::class); $directory = $filesystem->getDirectoryWrite(DirectoryList::ROOT); $source = $this->objectManager->create( @@ -1265,9 +1225,7 @@ public function testProductPositionInCategory() $this->_model->importData(); /** @var \Magento\Framework\App\ResourceConnection $resourceConnection */ - $resourceConnection = \Magento\TestFramework\Helper\Bootstrap::getObjectManager()->get( - \Magento\Framework\App\ResourceConnection::class - ); + $resourceConnection = $this->objectManager->get(\Magento\Framework\App\ResourceConnection::class); $tableName = $resourceConnection->getTableName('catalog_category_product'); $select = $resourceConnection->getConnection()->select()->from($tableName) ->where('category_id = ?', $category->getId()); @@ -1278,6 +1236,49 @@ public function testProductPositionInCategory() } } + /** + * Check new product position in category. + * + * @magentoAppArea adminhtml + * @magentoDbIsolation enabled + * @magentoAppIsolation enabled + * @magentoDataFixture Magento/Catalog/_files/category_product.php + * @return void + */ + public function testNewProductPositionInCategory() + { + $filesystem = $this->objectManager->create(\Magento\Framework\Filesystem::class); + + $directory = $filesystem->getDirectoryWrite(DirectoryList::ROOT); + $source = $this->objectManager->create( + \Magento\ImportExport\Model\Import\Source\Csv::class, + [ + 'file' => __DIR__ . '/_files/product_to_import_with_category.csv', + 'directory' => $directory, + ] + ); + $errors = $this->_model->setSource( + $source + )->setParameters( + [ + 'behavior' => \Magento\ImportExport\Model\Import::BEHAVIOR_APPEND, + 'entity' => 'catalog_product', + ] + )->validateData(); + + $this->assertTrue($errors->getErrorsCount() === 0); + $this->_model->importData(); + + /** @var ProductRepositoryInterface $productRepository */ + $productRepository = $this->objectManager->create(ProductRepositoryInterface::class); + /** @var ProductInterface $product */ + $product = $productRepository->get('simpleImported'); + $categoryLinks = $product->getExtensionAttributes('category_links')->getCategoryLinks(); + $position = $categoryLinks[0]->getPosition(); + + $this->assertEquals(0, $position); + } + /** * @return array */ @@ -1302,9 +1303,7 @@ public function testProductDuplicateCategories() $csvFixture = 'products_duplicate_category.csv'; // import data from CSV file $pathToFile = __DIR__ . '/_files/' . $csvFixture; - $filesystem = \Magento\TestFramework\Helper\Bootstrap::getObjectManager()->create( - \Magento\Framework\Filesystem::class - ); + $filesystem = $this->objectManager->create(\Magento\Framework\Filesystem::class); $directory = $filesystem->getDirectoryWrite(DirectoryList::ROOT); $source = $this->objectManager->create( @@ -1324,9 +1323,7 @@ public function testProductDuplicateCategories() $this->assertTrue($errors->getErrorsCount() === 0); /** @var \Magento\Catalog\Model\Category $category */ - $category = \Magento\TestFramework\Helper\Bootstrap::getObjectManager()->create( - \Magento\Catalog\Model\Category::class - ); + $category = $this->objectManager->create(\Magento\Catalog\Model\Category::class); $category->load(444); @@ -1338,7 +1335,7 @@ public function testProductDuplicateCategories() $this->_model->importData(); - $errorProcessor = \Magento\TestFramework\Helper\Bootstrap::getObjectManager()->get( + $errorProcessor = $this->objectManager->get( \Magento\ImportExport\Model\Import\ErrorProcessing\ProcessingErrorAggregator::class ); $errorCount = count($errorProcessor->getAllErrors()); @@ -1352,9 +1349,7 @@ public function testProductDuplicateCategories() $this->assertTrue($categoryAfter === null); /** @var \Magento\Catalog\Model\Product $product */ - $product = \Magento\TestFramework\Helper\Bootstrap::getObjectManager()->create( - \Magento\Catalog\Model\Product::class - ); + $product = $this->objectManager->create(\Magento\Catalog\Model\Product::class); $product->load(1); $categories = $product->getCategoryIds(); $this->assertTrue(count($categories) == 1); @@ -1377,9 +1372,7 @@ protected function loadCategoryByName($categoryName) */ public function testValidateUrlKeys($importFile, $expectedErrors) { - $filesystem = \Magento\TestFramework\Helper\Bootstrap::getObjectManager()->create( - \Magento\Framework\Filesystem::class - ); + $filesystem = $this->objectManager->create(\Magento\Framework\Filesystem::class); $directory = $filesystem->getDirectoryWrite(DirectoryList::ROOT); $source = $this->objectManager->create( @@ -1437,9 +1430,7 @@ public function validateUrlKeysDataProvider() */ public function testValidateUrlKeysMultipleStores() { - $filesystem = \Magento\TestFramework\Helper\Bootstrap::getObjectManager()->create( - \Magento\Framework\Filesystem::class - ); + $filesystem = $this->objectManager->create(\Magento\Framework\Filesystem::class); $directory = $filesystem->getDirectoryWrite(DirectoryList::ROOT); $source = $this->objectManager->create( @@ -1481,9 +1472,7 @@ public function testProductWithLinks() ]; // import data from CSV file $pathToFile = __DIR__ . '/_files/products_to_import_with_product_links.csv'; - $filesystem = \Magento\TestFramework\Helper\Bootstrap::getObjectManager()->create( - \Magento\Framework\Filesystem::class - ); + $filesystem = $this->objectManager->create(\Magento\Framework\Filesystem::class); $directory = $filesystem->getDirectoryWrite(DirectoryList::ROOT); $source = $this->objectManager->create( @@ -1505,13 +1494,10 @@ public function testProductWithLinks() $this->assertTrue($errors->getErrorsCount() == 0); $this->_model->importData(); - $objectManager = \Magento\TestFramework\Helper\Bootstrap::getObjectManager(); - $resource = $objectManager->get(\Magento\Catalog\Model\ResourceModel\Product::class); + $resource = $this->objectManager->get(\Magento\Catalog\Model\ResourceModel\Product::class); $productId = $resource->getIdBySku('simple4'); /** @var \Magento\Catalog\Model\Product $product */ - $product = \Magento\TestFramework\Helper\Bootstrap::getObjectManager()->create( - \Magento\Catalog\Model\Product::class - ); + $product = $this->objectManager->create(\Magento\Catalog\Model\Product::class); $product->load($productId); $productLinks = [ 'upsell' => $product->getUpSellProducts(), @@ -1539,8 +1525,7 @@ public function testExistingProductWithUrlKeys() 'simple2' => 'url-key2', 'simple3' => 'url-key3' ]; - $filesystem = \Magento\TestFramework\Helper\Bootstrap::getObjectManager() - ->create(\Magento\Framework\Filesystem::class); + $filesystem = $this->objectManager->create(\Magento\Framework\Filesystem::class); $directory = $filesystem->getDirectoryWrite(DirectoryList::ROOT); $source = $this->objectManager->create( \Magento\ImportExport\Model\Import\Source\Csv::class, @@ -1559,9 +1544,7 @@ public function testExistingProductWithUrlKeys() $this->assertTrue($errors->getErrorsCount() == 0); $this->_model->importData(); - $productRepository = \Magento\TestFramework\Helper\Bootstrap::getObjectManager()->create( - \Magento\Catalog\Api\ProductRepositoryInterface::class - ); + $productRepository = $this->objectManager->create(\Magento\Catalog\Api\ProductRepositoryInterface::class); foreach ($products as $productSku => $productUrlKey) { $this->assertEquals($productUrlKey, $productRepository->get($productSku)->getUrlKey()); } @@ -1690,8 +1673,7 @@ public function testProductWithUseConfigSettings() 'simple2' => true, 'simple3' => false ]; - $filesystem = \Magento\TestFramework\Helper\Bootstrap::getObjectManager() - ->create(\Magento\Framework\Filesystem::class); + $filesystem = $this->objectManager->create(\Magento\Framework\Filesystem::class); $directory = $filesystem->getDirectoryWrite(DirectoryList::ROOT); $source = $this->objectManager->create( \Magento\ImportExport\Model\Import\Source\Csv::class, @@ -1712,7 +1694,7 @@ public function testProductWithUseConfigSettings() foreach ($products as $sku => $manageStockUseConfig) { /** @var \Magento\CatalogInventory\Model\StockRegistry $stockRegistry */ - $stockRegistry = \Magento\TestFramework\Helper\Bootstrap::getObjectManager()->create( + $stockRegistry = $this->objectManager->create( \Magento\CatalogInventory\Model\StockRegistry::class ); $stockItem = $stockRegistry->getStockItemBySku($sku); @@ -1789,8 +1771,7 @@ function (ProductInterface $item) { $productSkuList = ['simple1', 'simple2', 'simple3']; foreach ($productSkuList as $sku) { try { - $productRepository = \Magento\TestFramework\Helper\Bootstrap::getObjectManager() - ->get(\Magento\Catalog\Api\ProductRepositoryInterface::class); + $productRepository = $this->objectManager->get(\Magento\Catalog\Api\ProductRepositoryInterface::class); $product = $productRepository->get($sku, true); if ($product->getId()) { $productRepository->delete($product); @@ -1836,9 +1817,7 @@ public function testProductWithWrappedAdditionalAttributes() $this->_model->importData(); /** @var \Magento\Catalog\Api\ProductRepositoryInterface $productRepository */ - $productRepository = \Magento\TestFramework\Helper\Bootstrap::getObjectManager()->create( - \Magento\Catalog\Api\ProductRepositoryInterface::class - ); + $productRepository = $this->objectManager->create(\Magento\Catalog\Api\ProductRepositoryInterface::class); /** @var \Magento\Eav\Api\AttributeOptionManagementInterface $multiselectOptions */ $multiselectOptions = $this->objectManager->get(\Magento\Eav\Api\AttributeOptionManagementInterface::class) @@ -2032,8 +2011,7 @@ public function validateRowDataProvider() */ public function testValidateData() { - $filesystem = \Magento\TestFramework\Helper\Bootstrap::getObjectManager() - ->create(\Magento\Framework\Filesystem::class); + $filesystem = $this->objectManager->create(\Magento\Framework\Filesystem::class); $directory = $filesystem->getDirectoryWrite(DirectoryList::ROOT); $source = $this->objectManager->create( @@ -2081,8 +2059,7 @@ public function testImportWithFilesystemImages() */ public function testImportDataChangeAttributeSet() { - $filesystem = \Magento\TestFramework\Helper\Bootstrap::getObjectManager() - ->create(\Magento\Framework\Filesystem::class); + $filesystem = $this->objectManager->create(\Magento\Framework\Filesystem::class); $directory = $filesystem->getDirectoryWrite(DirectoryList::ROOT); @@ -2107,18 +2084,14 @@ public function testImportDataChangeAttributeSet() $this->_model->importData(); /** @var \Magento\Catalog\Model\Product[] $products */ - $products[] = \Magento\TestFramework\Helper\Bootstrap::getObjectManager() - ->create(\Magento\Catalog\Model\ProductRepository::class)->get('simple'); - $products[] = \Magento\TestFramework\Helper\Bootstrap::getObjectManager() - ->create(\Magento\Catalog\Model\ProductRepository::class)->get('simple2'); + $products[] = $this->objectManager->create(\Magento\Catalog\Model\ProductRepository::class)->get('simple'); + $products[] = $this->objectManager->create(\Magento\Catalog\Model\ProductRepository::class)->get('simple2'); /** @var \Magento\Catalog\Model\Config $catalogConfig */ - $catalogConfig = \Magento\TestFramework\Helper\Bootstrap::getObjectManager() - ->create(\Magento\Catalog\Model\Config::class); + $catalogConfig = $this->objectManager->create(\Magento\Catalog\Model\Config::class); /** @var \Magento\Eav\Model\Config $eavConfig */ - $eavConfig = \Magento\TestFramework\Helper\Bootstrap::getObjectManager() - ->create(\Magento\Eav\Model\Config::class); + $eavConfig = $this->objectManager->create(\Magento\Eav\Model\Config::class); $entityTypeId = (int)$eavConfig->getEntityType(\Magento\Catalog\Model\Product::ENTITY) ->getId(); @@ -2135,12 +2108,9 @@ public function testImportDataChangeAttributeSet() public function testImportWithDifferentSkuCase() { /** @var \Magento\Catalog\Api\ProductRepositoryInterface $productRepository */ - $productRepository = \Magento\TestFramework\Helper\Bootstrap::getObjectManager()->create( - \Magento\Catalog\Api\ProductRepositoryInterface::class - ); + $productRepository = $this->objectManager->create(\Magento\Catalog\Api\ProductRepositoryInterface::class); /** @var \Magento\Framework\Api\SearchCriteria $searchCriteria */ - $searchCriteria = \Magento\TestFramework\Helper\Bootstrap::getObjectManager() - ->create(\Magento\Framework\Api\SearchCriteria::class); + $searchCriteria = $this->objectManager->create(\Magento\Framework\Api\SearchCriteria::class); $importedPrices = [ 'simple1' => 25, @@ -2153,8 +2123,7 @@ public function testImportWithDifferentSkuCase() 'simple3' => 333, ]; - $filesystem = \Magento\TestFramework\Helper\Bootstrap::getObjectManager() - ->create(\Magento\Framework\Filesystem::class); + $filesystem = $this->objectManager->create(\Magento\Framework\Filesystem::class); $directory = $filesystem->getDirectoryWrite(DirectoryList::ROOT); @@ -2219,7 +2188,6 @@ public function testImportWithDifferentSkuCase() /** * Test that product import with images for non-default store works properly. * - * @magentoDataIsolation enabled * @magentoDataFixture mediaImportImageFixture * @magentoAppIsolation enabled */ @@ -2266,7 +2234,6 @@ public function testProductsWithMultipleStoresWhenMediaIsDisabled() /** * Test that imported product stock status with backorders functionality enabled can be set to 'out of stock'. * - * @magentoDataIsolation enabled * @magentoAppIsolation enabled */ public function testImportWithBackordersEnabled() @@ -2276,6 +2243,20 @@ public function testImportWithBackordersEnabled() $this->assertFalse($product->getDataByKey('quantity_and_stock_status')['is_in_stock']); } + /** + * Test that imported product stock status with stock quantity > 0 and backorders functionality disabled + * can be set to 'out of stock'. + * + * @magentoDbIsolation enabled + * @magentoAppIsolation enabled + */ + public function testImportWithBackordersDisabled() + { + $this->importFile('products_to_import_with_backorders_disabled_and_not_0_qty.csv'); + $product = $this->getProductBySku('simple_new'); + $this->assertFalse($product->getDataByKey('quantity_and_stock_status')['is_in_stock']); + } + /** * Import file by providing import filename in parameters * @@ -2307,6 +2288,41 @@ private function importFile(string $fileName) $this->_model->importData(); } + /** + * Import file with non-existing images and skip-errors strategy. + * + * @return void + */ + public function testImportWithSkipErrorsAndNonExistingImage() + { + $fileName = 'products_error_img.csv'; + $filesystem = $this->objectManager->create(\Magento\Framework\Filesystem::class); + $directory = $filesystem->getDirectoryWrite(DirectoryList::ROOT); + $source = $this->objectManager->create( + \Magento\ImportExport\Model\Import\Source\Csv::class, + [ + 'file' => __DIR__ . '/_files/' . $fileName, + 'directory' => $directory, + ] + ); + $this->_model->getErrorAggregator()->initValidationStrategy('validation-skip-errors', 10); + $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(); + foreach ($this->_model->getErrorAggregator()->getAllErrors() as $error) { + $this->assertEquals('mediaUrlNotAvailable', $error->getErrorCode()); + } + } + /** * @magentoDataFixture Magento/Catalog/_files/product_simple.php * @magentoDbIsolation disabled @@ -2354,4 +2370,78 @@ public function testImportProductWithUpdateUrlKey() $collUrlRewrite->getFirstItem()->getRequestPath() ); } + + /** + * Test that product import with non existing images does not broke roles on existing images. + * + * @magentoDataFixture mediaImportImageFixture + * @magentoAppIsolation enabled + * @return void + */ + public function testSaveProductOnImportNonExistingImage() + { + $this->importDataForMediaTest('import_media.csv'); + + $product = $this->getProductBySku('simple_new'); + + $this->assertEquals('/m/a/magento_image.jpg', $product->getData('image')); + $this->assertEquals('/m/a/magento_small_image.jpg', $product->getData('small_image')); + $this->assertEquals('/m/a/magento_thumbnail.jpg', $product->getData('thumbnail')); + $this->assertEquals('/m/a/magento_image.jpg', $product->getData('swatch_image')); + + $this->importDataForMediaTest('import_media_non_existing_images.csv', 1); + + $this->assertNotEquals('/u/p/uploaded.jpg', $product->getData('image')); + $this->assertNotEquals('/u/p/uploaded.jpg', $product->getData('small_image')); + $this->assertNotEquals('/u/p/uploaded.jpg', $product->getData('thumbnail')); + $this->assertNotEquals('/u/p/uploaded.jpg', $product->getData('swatch_image')); + + $this->assertEquals('/m/a/magento_image.jpg', $product->getData('image')); + $this->assertEquals('/m/a/magento_small_image.jpg', $product->getData('small_image')); + $this->assertEquals('/m/a/magento_thumbnail.jpg', $product->getData('thumbnail')); + $this->assertEquals('/m/a/magento_image.jpg', $product->getData('swatch_image')); + } + + /** + * Test import product with wrong sku when On Error option is set as Continue Processing. + * + * @magentoDataFixture Magento/Catalog/_files/product_simple.php + * @magentoDbIsolation disabled + * @magentoAppIsolation enabled + * @return void + */ + public function testImportProductWithContinueOnError() + { + $filesystem = $this->objectManager->create(Filesystem::class); + $directory = $filesystem->getDirectoryWrite(DirectoryList::ROOT); + $source = $this->objectManager->create( + \Magento\ImportExport\Model\Import\Source\Csv::class, + [ + 'file' => __DIR__ . '/_files/product_import_qty_wrong_sku.csv', + 'directory' => $directory, + ] + ); + + $this->_model->setParameters( + [ + 'behavior' => Import::BEHAVIOR_APPEND, + Import::FIELD_NAME_VALIDATION_STRATEGY => + ProcessingErrorAggregatorInterface::VALIDATION_STRATEGY_SKIP_ERRORS, + 'entity' => 'catalog_product', + ] + )->setSource($source)->validateData(); + + $result = $this->_model->importData(); + + $this->assertTrue($result); + + /** @var $repository \Magento\Catalog\Model\ProductRepository */ + $repository = $this->objectManager->create(\Magento\Catalog\Model\ProductRepository::class); + $product = $repository->get('simple'); + /** @var $stockRegistry \Magento\CatalogInventory\Model\StockRegistry */ + $stockRegistry = $this->objectManager->create(\Magento\CatalogInventory\Model\StockRegistry::class); + + $stockItem = $stockRegistry->getStockItem($product->getId()); + $this->assertEquals(17, $stockItem->getQty()); + } } diff --git a/dev/tests/integration/testsuite/Magento/CatalogImportExport/Model/Import/_files/import_media_non_existing_images.csv b/dev/tests/integration/testsuite/Magento/CatalogImportExport/Model/Import/_files/import_media_non_existing_images.csv new file mode 100644 index 0000000000000..99fdf641d9a33 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/CatalogImportExport/Model/Import/_files/import_media_non_existing_images.csv @@ -0,0 +1,2 @@ +sku,store_view_code,attribute_set_code,product_type,categories,product_websites,name,description,short_description,weight,product_online,tax_class_name,visibility,price,special_price,special_price_from_date,special_price_to_date,url_key,meta_title,meta_keywords,meta_description,base_image,base_image_label,small_image,small_image_label,thumbnail_image,thumbnail_image_label,swatch_image,swatch_image_label1,created_at,updated_at,new_from_date,new_to_date,display_product_options_in,map_price,msrp_price,map_enabled,gift_message_available,custom_design,custom_design_from,custom_design_to,custom_layout_update,page_layout,product_options_container,msrp_display_actual_price_type,country_of_manufacture,additional_attributes,qty,out_of_stock_qty,use_config_min_qty,is_qty_decimal,allow_backorders,use_config_backorders,min_cart_qty,use_config_min_sale_qty,max_cart_qty,use_config_max_sale_qty,is_in_stock,notify_on_stock_below,use_config_notify_stock_qty,manage_stock,use_config_manage_stock,use_config_qty_increments,qty_increments,use_config_enable_qty_inc,enable_qty_increments,is_decimal_divided,website_id,related_skus,crosssell_skus,upsell_skus,additional_images,additional_image_labels,hide_from_product_page,custom_options,bundle_price_type,bundle_sku_type,bundle_price_view,bundle_weight_type,bundle_values,associated_skus +simple_new,,Default,simple,,base,New Product,,,,1,Taxable Goods,"Catalog, Search",10,,,,new-product,New Product,New Product,New Product,/u/p/uploaded.jpg,Image Label,/u/p/uploaded.jpg,Small Image Label,/u/p/uploaded.jpg,Thumbnail Label,magento_image.jpg,Image Label,10/20/15 07:05,10/20/15 07:05,,,Block after Info Column,,,,,,,,,,,,,"has_options=1,quantity_and_stock_status=In Stock,required_options=1",100,0,1,0,0,1,1,1,10000,1,1,1,1,1,0,1,1,0,0,0,1,,,,"magento_additional_image_one.jpg, magento_additional_image_two.jpg","Additional Image Label One,Additional Image Label Two",,,,,,,, diff --git a/dev/tests/integration/testsuite/Magento/CatalogImportExport/Model/Import/_files/product_import_qty_wrong_sku.csv b/dev/tests/integration/testsuite/Magento/CatalogImportExport/Model/Import/_files/product_import_qty_wrong_sku.csv new file mode 100644 index 0000000000000..6a9b7e0e1d62c --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/CatalogImportExport/Model/Import/_files/product_import_qty_wrong_sku.csv @@ -0,0 +1,3 @@ +sku,qty +simple100150,10 +simple,17 diff --git a/dev/tests/integration/testsuite/Magento/CatalogImportExport/Model/Import/_files/product_to_import_with_category.csv b/dev/tests/integration/testsuite/Magento/CatalogImportExport/Model/Import/_files/product_to_import_with_category.csv new file mode 100644 index 0000000000000..2bab84ec1ca57 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/CatalogImportExport/Model/Import/_files/product_to_import_with_category.csv @@ -0,0 +1,2 @@ +sku,product_type,store_view_code,name,price,attribute_set_code,categories +simpleImported,simple,,"simple Imported",25,Default,"Default Category/Category 1" diff --git a/dev/tests/integration/testsuite/Magento/CatalogImportExport/Model/Import/_files/products_error_img.csv b/dev/tests/integration/testsuite/Magento/CatalogImportExport/Model/Import/_files/products_error_img.csv new file mode 100755 index 0000000000000..97ac55e8e5a20 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/CatalogImportExport/Model/Import/_files/products_error_img.csv @@ -0,0 +1,11 @@ +sku,store_view_code,attribute_set_code,product_type,categories,product_websites,name,description,short_description,weight,product_online,tax_class_name,visibility,price,special_price,special_price_from_date,special_price_to_date,url_key,additional_images +simple1,,Default,simple,,base,simple1,,,,1,Taxable Goods,"Catalog, Search",100,,,,simple1,test.jpg +simple2,,Default,simple,,base,simple2,,,,2,Taxable Goods,"Catalog, Search",101,,,,simple2,test.jpg +simple3,,Default,simple,,base,simple3,,,,3,Taxable Goods,"Catalog, Search",102,,,,simple3,test.jpg +simple4,,Default,simple,,base,simple4,,,,4,Taxable Goods,"Catalog, Search",103,,,,simple4,test.jpg +simple5,,Default,simple,,base,simple5,,,,5,Taxable Goods,"Catalog, Search",104,,,,simple5,test.jpg +simple6,,Default,simple,,base,simple6,,,,6,Taxable Goods,"Catalog, Search",105,,,,simple6,test.jpg +simple7,,Default,simple,,base,simple7,,,,7,Taxable Goods,"Catalog, Search",106,,,,simple7,test.jpg +simple8,,Default,simple,,base,simple8,,,,8,Taxable Goods,"Catalog, Search",107,,,,simple8,test.jpg +simple9,,Default,simple,,base,simple9,,,,9,Taxable Goods,"Catalog, Search",108,,,,simple9,test.jpg +simple10,,Default,simple,,base,simple10,,,,10,Taxable Goods,"Catalog, Search",109,,,,simple10,test.jpg diff --git a/dev/tests/integration/testsuite/Magento/CatalogImportExport/Model/Import/_files/products_to_import_with_backorders_disabled_and_not_0_qty.csv b/dev/tests/integration/testsuite/Magento/CatalogImportExport/Model/Import/_files/products_to_import_with_backorders_disabled_and_not_0_qty.csv new file mode 100644 index 0000000000000..b22427a8af120 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/CatalogImportExport/Model/Import/_files/products_to_import_with_backorders_disabled_and_not_0_qty.csv @@ -0,0 +1,2 @@ +sku,store_view_code,attribute_set_code,product_type,categories,product_websites,name,description,short_description,weight,product_online,tax_class_name,visibility,price,special_price,special_price_from_date,special_price_to_date,url_key,meta_title,meta_keywords,meta_description,base_image,base_image_label,small_image,small_image_label,thumbnail_image,thumbnail_image_label,created_at,updated_at,new_from_date,new_to_date,display_product_options_in,map_price,msrp_price,map_enabled,gift_message_available,custom_design,custom_design_from,custom_design_to,custom_layout_update,page_layout,product_options_container,msrp_display_actual_price_type,country_of_manufacture,additional_attributes,qty,out_of_stock_qty,use_config_min_qty,is_qty_decimal,allow_backorders,use_config_backorders,min_cart_qty,use_config_min_sale_qty,max_cart_qty,use_config_max_sale_qty,is_in_stock,notify_on_stock_below,use_config_notify_stock_qty,manage_stock,use_config_manage_stock,use_config_qty_increments,qty_increments,use_config_enable_qty_inc,enable_qty_increments,is_decimal_divided,website_id,related_skus,crosssell_skus,upsell_skus,additional_images,additional_image_labels,hide_from_product_page,custom_options,bundle_price_type,bundle_sku_type,bundle_price_view,bundle_weight_type,bundle_values,associated_skus +simple_new,,Default,simple,,base,New Product,,,,1,Taxable Goods,"Catalog, Search",10,,,,new-product,New Product,New Product,New Product ,,,,,,,10/20/2015 7:05,10/20/2015 7:05,,,Block after Info Column,,,,,,,,,,,,,"has_options=1,quantity_and_stock_status=In Stock,required_options=1",100,0,1,0,0,0,1,1,10000,1,0,1,1,1,0,1,1,0,0,0,1,,,,,,,,,,,,, diff --git a/dev/tests/integration/testsuite/Magento/CatalogSearch/Model/Indexer/Fulltext/Action/FullTest.php b/dev/tests/integration/testsuite/Magento/CatalogSearch/Model/Indexer/Fulltext/Action/FullTest.php index 59ad91ae7b076..993462350b638 100644 --- a/dev/tests/integration/testsuite/Magento/CatalogSearch/Model/Indexer/Fulltext/Action/FullTest.php +++ b/dev/tests/integration/testsuite/Magento/CatalogSearch/Model/Indexer/Fulltext/Action/FullTest.php @@ -29,6 +29,8 @@ protected function setUp() } /** + * Testing fulltext index rebuild. + * * @magentoDataFixture Magento/CatalogSearch/_files/products_for_index.php * @magentoDataFixture Magento/CatalogSearch/_files/product_configurable_not_available.php * @magentoDataFixture Magento/Framework/Search/_files/product_configurable.php @@ -58,6 +60,8 @@ public function testGetIndexData() } /** + * Prepare and return expected index data. + * * @return array */ private function getExpectedIndexData() @@ -68,32 +72,49 @@ private function getExpectedIndexData() $nameId = $attributeRepository->get(ProductInterface::NAME)->getAttributeId(); /** @see dev/tests/integration/testsuite/Magento/Framework/Search/_files/configurable_attribute.php */ $configurableId = $attributeRepository->get('test_configurable')->getAttributeId(); + $statusId = $attributeRepository->get(ProductInterface::STATUS)->getAttributeId(); + $taxClassId = $attributeRepository + ->get(\Magento\Customer\Api\Data\GroupInterface::TAX_CLASS_ID) + ->getAttributeId(); + return [ 'configurable' => [ $skuId => 'configurable', $configurableId => 'Option 1 | Option 2', $nameId => 'Configurable Product | Configurable OptionOption 1 | Configurable OptionOption 2', + $taxClassId => 'Taxable Goods | Taxable Goods | Taxable Goods', + $statusId => 'Enabled | Enabled | Enabled', ], 'index_enabled' => [ $skuId => 'index_enabled', $nameId => 'index enabled', + $taxClassId => 'Taxable Goods', + $statusId => 'Enabled', ], 'index_visible_search' => [ $skuId => 'index_visible_search', $nameId => 'index visible search', + $taxClassId => 'Taxable Goods', + $statusId => 'Enabled', ], 'index_visible_category' => [ $skuId => 'index_visible_category', $nameId => 'index visible category', + $taxClassId => 'Taxable Goods', + $statusId => 'Enabled', ], 'index_visible_both' => [ $skuId => 'index_visible_both', $nameId => 'index visible both', + $taxClassId => 'Taxable Goods', + $statusId => 'Enabled', ] ]; } /** + * Testing fulltext index rebuild with configurations. + * * @magentoDataFixture Magento/ConfigurableProduct/_files/product_configurable.php */ public function testRebuildStoreIndexConfigurable() @@ -114,6 +135,8 @@ public function testRebuildStoreIndexConfigurable() } /** + * Get product Id by its SKU. + * * @param string $sku * @return int */ diff --git a/dev/tests/integration/testsuite/Magento/CatalogUrlRewrite/Model/CategoryUrlRewriteGeneratorTest.php b/dev/tests/integration/testsuite/Magento/CatalogUrlRewrite/Model/CategoryUrlRewriteGeneratorTest.php index a6cd7d195731c..8dea5623a74c2 100644 --- a/dev/tests/integration/testsuite/Magento/CatalogUrlRewrite/Model/CategoryUrlRewriteGeneratorTest.php +++ b/dev/tests/integration/testsuite/Magento/CatalogUrlRewrite/Model/CategoryUrlRewriteGeneratorTest.php @@ -14,6 +14,7 @@ /** * @magentoAppArea adminhtml + * @SuppressWarnings(PHPMD.CouplingBetweenObjects) */ class CategoryUrlRewriteGeneratorTest extends \PHPUnit\Framework\TestCase { @@ -263,4 +264,93 @@ protected function assertResults($expected, $actual) ); } } + + /** + * Get all actual url paths. + * + * @param array $filter + * @return array + */ + private function getActualUrlPaths(array $filter) + { + /** @var \Magento\UrlRewrite\Model\UrlFinderInterface $urlFinder */ + $urlFinder = $this->objectManager->get(\Magento\UrlRewrite\Model\UrlFinderInterface::class); + $actualResults = []; + foreach ($urlFinder->findAllByData($filter) as $url) { + $actualResults[] = [ + $url->getRequestPath(), + $url->getTargetPath(), + $url->getStoreId(), + ]; + } + + return $actualResults; + } + + /** + * Test getting url path. + * + * @magentoDataFixture Magento/Store/_files/second_store.php + * @magentoDataFixture Magento/CatalogUrlRewrite/_files/categories.php + * @magentoDbIsolation enabled + * @magentoAppIsolation enabled + */ + public function testGetUrlPath() + { + /** @var \Magento\Catalog\Api\CategoryRepositoryInterface $repository */ + $repository = $this->objectManager->get(\Magento\Catalog\Api\CategoryRepositoryInterface::class); + + /** @var \Magento\Store\Api\StoreRepositoryInterface $storeRepository */ + $storeRepository = $this->objectManager->get(\Magento\Store\Api\StoreRepositoryInterface::class); + $storeId = $storeRepository->get('fixture_second_store')->getId(); + + $storeManager = $this->objectManager->get(\Magento\Store\Model\StoreManagerInterface::class); + $store = $storeManager->getStore($storeId); + $storeManager->setCurrentStore($store->getCode()); + + $categoryFilter = [ + UrlRewrite::ENTITY_TYPE => CategoryUrlRewriteGenerator::ENTITY_TYPE, + UrlRewrite::ENTITY_ID => [5], + ]; + + $category = $repository->get(5); + $category->addData( + [ + 'use_default' => [ + 'url_key' => 0, + ], + 'url_key_create_redirect' => 0, + 'url_key' => 'url-key', + ]); + $category->setStoreId($storeId); + $repository->save($category); + + $actualResults = $this->getActualUrlPaths($categoryFilter); + $categoryExpectedResult = [ + ['category-1/category-1-1/category-1-1-1.html', 'catalog/category/view/id/5', 1], + ['category-1/category-1-1/url-key.html', 'catalog/category/view/id/5', $storeId], + ]; + + $this->assertResults($categoryExpectedResult, $actualResults); + + $category = $repository->get(5); + + $category->addData( + [ + 'use_default' => [ + 'url_key' => 1, + ], + 'url_key' => '', + ]); + + $repository->save($category); + + $actualResults = $this->getActualUrlPaths($categoryFilter); + $categoryExpectedResult = [ + ['category-1/category-1-1/category-1-1-1.html', 'catalog/category/view/id/5', 1], + ['category-1/category-1-1/category-1-1-1.html', 'catalog/category/view/id/5', $storeId], + ]; + + $this->assertResults($categoryExpectedResult, $actualResults); + } } diff --git a/dev/tests/integration/testsuite/Magento/Cms/Model/BlockTest.php b/dev/tests/integration/testsuite/Magento/Cms/Model/BlockTest.php index 9b2bb67c55da1..bc419f527830d 100644 --- a/dev/tests/integration/testsuite/Magento/Cms/Model/BlockTest.php +++ b/dev/tests/integration/testsuite/Magento/Cms/Model/BlockTest.php @@ -6,10 +6,9 @@ namespace Magento\Cms\Model; use Magento\Cms\Api\BlockRepositoryInterface; -use Magento\Cms\Model\BlockFactory; use Magento\Cms\Model\ResourceModel\Block; +use Magento\Framework\App\ResourceConnection; use Magento\Framework\ObjectManagerInterface; -use Magento\Framework\Stdlib\DateTime\DateTime; use Magento\TestFramework\Helper\Bootstrap; use PHPUnit\Framework\TestCase; @@ -38,42 +37,53 @@ class BlockTest extends TestCase */ private $blockRepository; + /** + * @inheritdoc + */ protected function setUp() { $this->objectManager = Bootstrap::getObjectManager(); - - /** @var BlockFactory $blockFactory */ - /** @var Block $blockResource */ $this->blockResource = $this->objectManager->create(Block::class); $this->blockFactory = $this->objectManager->create(BlockFactory::class); $this->blockRepository = $this->objectManager->create(BlockRepositoryInterface::class); } /** - * Test UpdateTime + * Tests update time. + * * @param array $blockData - * @throws \Exception + * @return void * @magentoDbIsolation enabled * @dataProvider testUpdateTimeDataProvider */ public function testUpdateTime(array $blockData) { + /** @var \Magento\Framework\DB\Adapter\AdapterInterface $db */ + $db = $this->objectManager->get(ResourceConnection::class) + ->getConnection(ResourceConnection::DEFAULT_CONNECTION); + # Prepare and save the temporary block $tempBlock = $this->blockFactory->create(); $tempBlock->setData($blockData); + $beforeTimestamp = $db->fetchOne('SELECT UNIX_TIMESTAMP()'); $this->blockResource->save($tempBlock); + $afterTimestamp = $db->fetchOne('SELECT UNIX_TIMESTAMP()'); # Load previously created block and compare update_time field $block = $this->blockRepository->getById($tempBlock->getId()); - $date = $this->objectManager->get(DateTime::class)->date(); - $this->assertEquals($date, $block->getUpdateTime()); + $blockTimestamp = strtotime($block->getUpdateTime()); + + /** These checks prevent a race condition */ + $this->assertGreaterThanOrEqual($beforeTimestamp, $blockTimestamp); + $this->assertLessThanOrEqual($afterTimestamp, $blockTimestamp); } /** - * Data provider "testUpdateTime" method + * Data provider "testUpdateTime" method. + * * @return array */ - public function testUpdateTimeDataProvider() + public function testUpdateTimeDataProvider(): array { return [ [ @@ -82,9 +92,9 @@ public function testUpdateTimeDataProvider() 'stores' => [0], 'identifier' => 'test-identifier', 'content' => 'Test content', - 'is_active' => 1 - ] - ] + 'is_active' => 1, + ], + ], ]; } } diff --git a/dev/tests/integration/testsuite/Magento/Cms/Model/PageTest.php b/dev/tests/integration/testsuite/Magento/Cms/Model/PageTest.php index c8040861b08eb..c3553717d0224 100644 --- a/dev/tests/integration/testsuite/Magento/Cms/Model/PageTest.php +++ b/dev/tests/integration/testsuite/Magento/Cms/Model/PageTest.php @@ -6,23 +6,32 @@ namespace Magento\Cms\Model; use Magento\Cms\Api\PageRepositoryInterface; -use Magento\Framework\Stdlib\DateTime\DateTime; +use Magento\Framework\App\ResourceConnection; /** * @magentoAppArea adminhtml */ class PageTest extends \PHPUnit\Framework\TestCase { + /** + * @var \Magento\Framework\ObjectManagerInterface + */ + private $objectManager; + + /** + * @inheritdoc + */ protected function setUp() { - $user = \Magento\TestFramework\Helper\Bootstrap::getObjectManager()->create( + $this->objectManager = \Magento\TestFramework\Helper\Bootstrap::getObjectManager(); + $user = $this->objectManager->create( \Magento\User\Model\User::class )->loadByUsername( \Magento\TestFramework\Bootstrap::ADMIN_NAME ); /** @var $session \Magento\Backend\Model\Auth\Session */ - $session = \Magento\TestFramework\Helper\Bootstrap::getObjectManager()->get( + $session = $this->objectManager->get( \Magento\Backend\Model\Auth\Session::class ); $session->setUser($user); @@ -30,13 +39,15 @@ protected function setUp() /** * @magentoDbIsolation enabled - * @dataProvider generateIdentifierFromTitleDataProvider + * @dataProvider generateIdentifierFromTitleDataProvider + * @param array $data + * @param string $expectedIdentifier + * @return void */ - public function testGenerateIdentifierFromTitle($data, $expectedIdentifier) + public function testGenerateIdentifierFromTitle(array $data, string $expectedIdentifier) { - $objectManager = \Magento\TestFramework\Helper\Bootstrap::getObjectManager(); - /** @var \Magento\Cms\Model\Page $page */ - $page = $objectManager->create(\Magento\Cms\Model\Page::class); + /** @var Page $page */ + $page = $this->objectManager->create(Page::class); $page->setData($data); $page->save(); $this->assertEquals($expectedIdentifier, $page->getIdentifier()); @@ -44,31 +55,43 @@ public function testGenerateIdentifierFromTitle($data, $expectedIdentifier) /** * @magentoDbIsolation enabled + * @return void */ public function testUpdateTime() { - $objectManager = \Magento\TestFramework\Helper\Bootstrap::getObjectManager(); - /** @var \Magento\Cms\Model\Page $page */ - $page = $objectManager->create(\Magento\Cms\Model\Page::class); + /** @var \Magento\Framework\DB\Adapter\AdapterInterface $db */ + $db = $this->objectManager->get(ResourceConnection::class) + ->getConnection(ResourceConnection::DEFAULT_CONNECTION); + + /** @var Page $page */ + $page = $this->objectManager->create(Page::class); $page->setData(['title' => 'Test', 'stores' => [1]]); + $beforeTimestamp = $db->fetchOne('SELECT UNIX_TIMESTAMP()'); $page->save(); - $page = $objectManager->get(PageRepositoryInterface::class)->getById($page->getId()); - $date = $objectManager->get(DateTime::class)->date(); - $this->assertEquals($date, $page->getUpdateTime()); + $afterTimestamp = $db->fetchOne('SELECT UNIX_TIMESTAMP()'); + $page = $this->objectManager->get(PageRepositoryInterface::class)->getById($page->getId()); + $pageTimestamp = strtotime($page->getUpdateTime()); + + /** These checks prevent a race condition */ + $this->assertGreaterThanOrEqual($beforeTimestamp, $pageTimestamp); + $this->assertLessThanOrEqual($afterTimestamp, $pageTimestamp); } - public function generateIdentifierFromTitleDataProvider() + /** + * @return array + */ + public function generateIdentifierFromTitleDataProvider(): array { return [ ['data' => ['title' => 'Test title', 'stores' => [1]], 'expectedIdentifier' => 'test-title'], [ 'data' => ['title' => 'Кирилический заголовок', 'stores' => [1]], - 'expectedIdentifier' => 'kirilicheskij-zagolovok' + 'expectedIdentifier' => 'kirilicheskij-zagolovok', ], [ 'data' => ['title' => 'Test title', 'identifier' => 'custom-identifier', 'stores' => [1]], - 'expectedIdentifier' => 'custom-identifier' - ] + 'expectedIdentifier' => 'custom-identifier', + ], ]; } } diff --git a/dev/tests/integration/testsuite/Magento/Config/Console/Command/ConfigSetCommandTest.php b/dev/tests/integration/testsuite/Magento/Config/Console/Command/ConfigSetCommandTest.php index 3f333a36c9c93..a1998c6c89536 100644 --- a/dev/tests/integration/testsuite/Magento/Config/Console/Command/ConfigSetCommandTest.php +++ b/dev/tests/integration/testsuite/Magento/Config/Console/Command/ConfigSetCommandTest.php @@ -27,7 +27,7 @@ /** * Tests the different flows of config:set command. * - * {@inheritdoc} + * @inheritdoc * @SuppressWarnings(PHPMD.CouplingBetweenObjects) * @magentoDbIsolation enabled */ @@ -291,8 +291,7 @@ public function runExtendedDataProvider() * @param string $scope * @param $scopeCode string|null * @dataProvider configSetValidationErrorDataProvider - * - * @magentoDbIsolation enabled + * @magentoDbIsolation disabled */ public function testConfigSetValidationError( $path, @@ -306,6 +305,7 @@ public function testConfigSetValidationError( /** * Data provider for testConfigSetValidationError + * * @return array */ public function configSetValidationErrorDataProvider() @@ -398,7 +398,6 @@ public function testConfigSetCurrency() * Saving values with successful validation * * @dataProvider configSetValidDataProvider - * * @magentoDbIsolation enabled */ public function testConfigSetValid() 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 66091c108f0b2..f02ab6acf7ac3 100644 --- a/dev/tests/integration/testsuite/Magento/ConfigurableProduct/Model/ResourceModel/Product/Indexer/Price/ConfigurableTest.php +++ b/dev/tests/integration/testsuite/Magento/ConfigurableProduct/Model/ResourceModel/Product/Indexer/Price/ConfigurableTest.php @@ -12,11 +12,18 @@ use Magento\Store\Model\Store; use Magento\Store\Model\StoreManagerInterface; use Magento\TestFramework\Helper\Bootstrap; +use Magento\CatalogInventory\Model\Stock; +use Magento\CatalogInventory\Api\StockItemRepositoryInterface; +use PHPUnit\Framework\TestCase; +use Magento\Catalog\Api\Data\ProductInterface; /** + * Configurable test + * + * @SuppressWarnings(PHPMD.CouplingBetweenObjects) * @magentoAppArea adminhtml */ -class ConfigurableTest extends \PHPUnit\Framework\TestCase +class ConfigurableTest extends TestCase { /** * @var StoreManagerInterface @@ -28,26 +35,36 @@ class ConfigurableTest extends \PHPUnit\Framework\TestCase */ private $productRepository; + /** + * @var StockItemRepositoryInterface + */ + private $stockRepository; + + /** + * @inheritdoc + */ protected function setUp() { $this->storeManager = Bootstrap::getObjectManager()->get(StoreManagerInterface::class); $this->productRepository = Bootstrap::getObjectManager()->get(ProductRepositoryInterface::class); + $this->stockRepository = Bootstrap::getObjectManager()->get(StockItemRepositoryInterface::class); } /** + * Test get product final price if one of child is disabled + * * @magentoDataFixture Magento/ConfigurableProduct/_files/product_configurable.php * @magentoDbIsolation disabled + * + * @return void + * @throws \Magento\Framework\Exception\CouldNotSaveException + * @throws \Magento\Framework\Exception\InputException + * @throws \Magento\Framework\Exception\NoSuchEntityException + * @throws \Magento\Framework\Exception\StateException */ public function testGetProductFinalPriceIfOneOfChildIsDisabled() { - /** @var Collection $collection */ - $collection = Bootstrap::getObjectManager()->get(CollectionFactory::class) - ->create(); - $configurableProduct = $collection - ->addIdFilter([1]) - ->addMinimalPrice() - ->load() - ->getFirstItem(); + $configurableProduct = $this->getConfigurableProductFromCollection(); $this->assertEquals(10, $configurableProduct->getMinimalPrice()); $childProduct = $this->productRepository->getById(10, false, null, true); @@ -58,31 +75,25 @@ public function testGetProductFinalPriceIfOneOfChildIsDisabled() $this->productRepository->save($childProduct); $this->storeManager->setCurrentStore($currentStoreId); - /** @var Collection $collection */ - $collection = Bootstrap::getObjectManager()->get(CollectionFactory::class) - ->create(); - $configurableProduct = $collection - ->addIdFilter([1]) - ->addMinimalPrice() - ->load() - ->getFirstItem(); + $configurableProduct = $this->getConfigurableProductFromCollection(); $this->assertEquals(20, $configurableProduct->getMinimalPrice()); } /** + * Test get product final price if one of child is disabled per store + * * @magentoDataFixture Magento/ConfigurableProduct/_files/product_configurable.php * @magentoDbIsolation disabled + * + * @return void + * @throws \Magento\Framework\Exception\CouldNotSaveException + * @throws \Magento\Framework\Exception\InputException + * @throws \Magento\Framework\Exception\NoSuchEntityException + * @throws \Magento\Framework\Exception\StateException */ public function testGetProductFinalPriceIfOneOfChildIsDisabledPerStore() { - /** @var Collection $collection */ - $collection = Bootstrap::getObjectManager()->get(CollectionFactory::class) - ->create(); - $configurableProduct = $collection - ->addIdFilter([1]) - ->addMinimalPrice() - ->load() - ->getFirstItem(); + $configurableProduct = $this->getConfigurableProductFromCollection(); $this->assertEquals(10, $configurableProduct->getMinimalPrice()); $childProduct = $this->productRepository->getById(10, false, null, true); @@ -95,14 +106,53 @@ public function testGetProductFinalPriceIfOneOfChildIsDisabledPerStore() $this->productRepository->save($childProduct); $this->storeManager->setCurrentStore($currentStoreId); + $configurableProduct = $this->getConfigurableProductFromCollection(); + $this->assertEquals(20, $configurableProduct->getMinimalPrice()); + } + + /** + * Test get product minimal price if one child is out of stock + * + * @magentoConfigFixture current_store cataloginventory/options/show_out_of_stock 1 + * @magentoDataFixture Magento/ConfigurableProduct/_files/product_configurable.php + * @magentoDbIsolation disabled + * + * @return void + * @throws \Magento\Framework\Exception\NoSuchEntityException + */ + public function testGetProductMinimalPriceIfOneOfChildIsOutOfStock() + { + $configurableProduct = $this->getConfigurableProductFromCollection(); + $this->assertEquals(10, $configurableProduct->getMinimalPrice()); + + $childProduct = $this->productRepository->get('simple_10', false, null, true); + $stockItem = $childProduct->getExtensionAttributes()->getStockItem(); + $stockItem->setIsInStock(Stock::STOCK_OUT_OF_STOCK); + $this->stockRepository->save($stockItem); + + $configurableProduct = $this->getConfigurableProductFromCollection(); + $this->assertEquals(20, $configurableProduct->getMinimalPrice()); + } + + /** + * Retrieve configurable product. + * Returns Configurable product that was created by Magento/ConfigurableProduct/_files/product_configurable.php + * fixture + * + * @return ProductInterface + */ + private function getConfigurableProductFromCollection(): ProductInterface + { /** @var Collection $collection */ $collection = Bootstrap::getObjectManager()->get(CollectionFactory::class) ->create(); + /** @var ProductInterface $configurableProduct */ $configurableProduct = $collection ->addIdFilter([1]) ->addMinimalPrice() ->load() ->getFirstItem(); - $this->assertEquals(20, $configurableProduct->getMinimalPrice()); + + return $configurableProduct; } } diff --git a/dev/tests/integration/testsuite/Magento/Customer/Controller/AccountTest.php b/dev/tests/integration/testsuite/Magento/Customer/Controller/AccountTest.php index ff0d838cb6f82..4fed6c84ab09d 100644 --- a/dev/tests/integration/testsuite/Magento/Customer/Controller/AccountTest.php +++ b/dev/tests/integration/testsuite/Magento/Customer/Controller/AccountTest.php @@ -19,10 +19,13 @@ use Magento\Framework\App\Http; use Magento\Framework\Data\Form\FormKey; use Magento\Framework\Message\MessageInterface; -use Magento\Store\Model\ScopeInterface; use Magento\TestFramework\Helper\Bootstrap; use Magento\TestFramework\Request; use Magento\TestFramework\Response; +use Magento\TestFramework\Mail\Template\TransportBuilderMock; +use Magento\Framework\Serialize\Serializer\Json; +use Magento\Framework\Stdlib\CookieManagerInterface; +use Magento\Theme\Controller\Result\MessagePlugin; use Zend\Stdlib\Parameters; /** @@ -30,6 +33,20 @@ */ class AccountTest extends \Magento\TestFramework\TestCase\AbstractController { + /** + * @var TransportBuilderMock + */ + private $transportBuilderMock; + + /** + * @inheritdoc + */ + protected function setUp() + { + parent::setUp(); + $this->transportBuilderMock = $this->_objectManager->get(TransportBuilderMock::class); + } + /** * Login the user * @@ -102,11 +119,7 @@ public function testForgotPasswordEmailMessageWithSpecialCharacters() $this->dispatch('customer/account/forgotPasswordPost'); $this->assertRedirect($this->stringContains('customer/account/')); - /** @var \Magento\TestFramework\Mail\Template\TransportBuilderMock $transportBuilder */ - $transportBuilder = $this->_objectManager->get( - \Magento\TestFramework\Mail\Template\TransportBuilderMock::class - ); - $subject = $transportBuilder->getSentMessage()->getSubject(); + $subject = $this->transportBuilderMock->getSentMessage()->getSubject(); $this->assertContains( 'Test special\' characters', $subject @@ -228,26 +241,10 @@ public function testNoFormKeyCreatePostAction() /** * @magentoDbIsolation enabled * @magentoAppIsolation enabled + * @magentoDataFixture Magento/Customer/_files/customer_confirmation_config_disable.php */ public function testNoConfirmCreatePostAction() { - /** @var \Magento\Framework\App\Config\MutableScopeConfigInterface $mutableScopeConfig */ - $mutableScopeConfig = Bootstrap::getObjectManager() - ->get(\Magento\Framework\App\Config\MutableScopeConfigInterface::class); - - $scopeValue = $mutableScopeConfig->getValue( - 'customer/create_account/confirm', - ScopeInterface::SCOPE_WEBSITES, - null - ); - - $mutableScopeConfig->setValue( - 'customer/create_account/confirm', - 0, - ScopeInterface::SCOPE_WEBSITES, - null - ); - $this->fillRequestWithAccountDataAndFormKey(); $this->dispatch('customer/account/createPost'); $this->assertRedirect($this->stringEndsWith('customer/account/')); @@ -255,38 +252,15 @@ public function testNoConfirmCreatePostAction() $this->equalTo(['Thank you for registering with Main Website Store.']), MessageInterface::TYPE_SUCCESS ); - - $mutableScopeConfig->setValue( - 'customer/create_account/confirm', - $scopeValue, - ScopeInterface::SCOPE_WEBSITES, - null - ); } /** * @magentoDbIsolation enabled * @magentoAppIsolation enabled + * @magentoDataFixture Magento/Customer/_files/customer_confirmation_config_enable.php */ public function testWithConfirmCreatePostAction() { - /** @var \Magento\Framework\App\Config\MutableScopeConfigInterface $mutableScopeConfig */ - $mutableScopeConfig = Bootstrap::getObjectManager() - ->get(\Magento\Framework\App\Config\MutableScopeConfigInterface::class); - - $scopeValue = $mutableScopeConfig->getValue( - 'customer/create_account/confirm', - ScopeInterface::SCOPE_WEBSITES, - null - ); - - $mutableScopeConfig->setValue( - 'customer/create_account/confirm', - 1, - ScopeInterface::SCOPE_WEBSITES, - null - ); - $this->fillRequestWithAccountDataAndFormKey(); $this->dispatch('customer/account/createPost'); $this->assertRedirect($this->stringContains('customer/account/index/')); @@ -298,13 +272,6 @@ public function testWithConfirmCreatePostAction() ]), MessageInterface::TYPE_SUCCESS ); - - $mutableScopeConfig->setValue( - 'customer/create_account/confirm', - $scopeValue, - ScopeInterface::SCOPE_WEBSITES, - null - ); } /** @@ -690,6 +657,46 @@ public function testLoginPostRedirect($redirectDashboard, string $redirectUrl) $this->assertTrue($this->_objectManager->get(Session::class)->isLoggedIn()); } + /** + * Test that confirmation email address displays special characters correctly. + * + * @magentoDbIsolation enabled + * @magentoDataFixture Magento/Customer/_files/customer_confirmation_email_address_with_special_chars.php + * + * @return void + */ + public function testConfirmationEmailWithSpecialCharacters() + { + $email = 'customer+confirmation@example.com'; + $this->dispatch('customer/account/confirmation/email/customer%2Bconfirmation%40email.com'); + $this->getRequest()->setPostValue('email', $email); + $this->dispatch('customer/account/confirmation/email/customer%2Bconfirmation%40email.com'); + + $this->assertRedirect($this->stringContains('customer/account/index')); + $this->assertSessionMessages( + $this->equalTo(['Please check your email for confirmation key.']), + MessageInterface::TYPE_SUCCESS + ); + + /** @var $message \Magento\Framework\Mail\Message */ + $message = $this->transportBuilderMock->getSentMessage(); + $rawMessage = $message->getRawMessage(); + + $this->assertContains('To: ' . $email, $rawMessage); + + $content = $message->getBody()->getPartContent(0); + $confirmationUrl = $this->getConfirmationUrlFromMessageContent($content); + $this->setRequestInfo($confirmationUrl, 'confirm'); + $this->clearCookieMessagesList(); + $this->dispatch($confirmationUrl); + + $this->assertRedirect($this->stringContains('customer/account/index')); + $this->assertSessionMessages( + $this->equalTo(['Thank you for registering with Main Website Store.']), + MessageInterface::TYPE_SUCCESS + ); + } + /** * Data provider for testLoginPostRedirect. * @@ -705,16 +712,17 @@ public function loginPostRedirectDataProvider() } /** + * @param string $email * @return void */ - private function fillRequestWithAccountData() + private function fillRequestWithAccountData(string $email = 'test1@email.com') { $this->getRequest() ->setMethod('POST') ->setParam('firstname', 'firstname1') ->setParam('lastname', 'lastname1') ->setParam('company', '') - ->setParam('email', 'test1@email.com') + ->setParam('email', $email) ->setParam('password', '_Password1') ->setParam('password_confirmation', '_Password1') ->setParam('telephone', '5123334444') @@ -731,11 +739,12 @@ private function fillRequestWithAccountData() } /** + * @param string $email * @return void */ - private function fillRequestWithAccountDataAndFormKey() + private function fillRequestWithAccountDataAndFormKey(string $email = 'test1@email.com') { - $this->fillRequestWithAccountData(); + $this->fillRequestWithAccountData($email); $formKey = $this->_objectManager->get(FormKey::class); $this->getRequest()->setParam('form_key', $formKey->getFormKey()); } @@ -805,4 +814,53 @@ private function assertResponseRedirect(Response $response, string $redirectUrl) $this->assertTrue($response->isRedirect()); $this->assertSame($redirectUrl, $response->getHeader('Location')->getUri()); } + + /** + * Add new request info (request uri, path info, action name). + * + * @param string $uri + * @param string $actionName + * @return void + */ + private function setRequestInfo(string $uri, string $actionName) + { + $this->getRequest() + ->setRequestUri($uri) + ->setPathInfo() + ->setActionName($actionName); + } + + /** + * Clear cookie messages list. + * + * @return void + */ + private function clearCookieMessagesList() + { + $cookieManager = $this->_objectManager->get(CookieManagerInterface::class); + $jsonSerializer = $this->_objectManager->get(Json::class); + $cookieManager->setPublicCookie( + MessagePlugin::MESSAGES_COOKIES_NAME, + $jsonSerializer->serialize([]) + ); + } + + /** + * Get confirmation URL from message content. + * + * @param string $content + * @return string + */ + private function getConfirmationUrlFromMessageContent(string $content): string + { + $confirmationUrl = ''; + + if (preg_match('<a\s*href="(?<url>.*?)".*>', $content, $matches)) { + $confirmationUrl = $matches['url']; + $confirmationUrl = str_replace('http://localhost/index.php/', '', $confirmationUrl); + $confirmationUrl = html_entity_decode($confirmationUrl); + } + + return $confirmationUrl; + } } diff --git a/dev/tests/integration/testsuite/Magento/Customer/Model/CustomerMetadataTest.php b/dev/tests/integration/testsuite/Magento/Customer/Model/CustomerMetadataTest.php index 5425af856bd9f..d013309eda3dc 100644 --- a/dev/tests/integration/testsuite/Magento/Customer/Model/CustomerMetadataTest.php +++ b/dev/tests/integration/testsuite/Magento/Customer/Model/CustomerMetadataTest.php @@ -54,16 +54,23 @@ protected function setUp() public function testGetCustomAttributesMetadata() { - $customAttributesMetadata = $this->service->getCustomAttributesMetadata(); - $this->assertCount(0, $customAttributesMetadata, "Invalid number of attributes returned."); + $customAttributesMetadataQty = count($this->service->getCustomAttributesMetadata()) ; // Verify the consistency of getCustomerAttributeMetadata() function from the 2nd call of the same service - $customAttributesMetadata1 = $this->service->getCustomAttributesMetadata(); - $this->assertCount(0, $customAttributesMetadata1, "Invalid number of attributes returned."); + $customAttributesMetadata1Qty = count($this->service->getCustomAttributesMetadata()); + $this->assertEquals( + $customAttributesMetadataQty, + $customAttributesMetadata1Qty, + "Invalid number of attributes returned." + ); // Verify the consistency of getCustomAttributesMetadata() function from the 2nd service - $customAttributesMetadata2 = $this->serviceTwo->getCustomAttributesMetadata(); - $this->assertCount(0, $customAttributesMetadata2, "Invalid number of attributes returned."); + $customAttributesMetadata2Qty = count($this->serviceTwo->getCustomAttributesMetadata()); + $this->assertEquals( + $customAttributesMetadataQty, + $customAttributesMetadata2Qty, + "Invalid number of attributes returned." + ); } public function testGetNestedOptionsCustomerAttributesMetadata() diff --git a/dev/tests/integration/testsuite/Magento/Customer/_files/customer_confirmation_config_disable.php b/dev/tests/integration/testsuite/Magento/Customer/_files/customer_confirmation_config_disable.php new file mode 100644 index 0000000000000..7d4e451db514b --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Customer/_files/customer_confirmation_config_disable.php @@ -0,0 +1,21 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +declare(strict_types=1); + +use Magento\Store\Model\ScopeInterface; +use Magento\Framework\App\Config\MutableScopeConfigInterface; +use Magento\TestFramework\Helper\Bootstrap; + +$objectManager = Bootstrap::getObjectManager(); +$mutableScopeConfig = $objectManager->create(MutableScopeConfigInterface::class); + +$mutableScopeConfig->setValue( + 'customer/create_account/confirm', + 0, + ScopeInterface::SCOPE_WEBSITES, + null +); diff --git a/dev/tests/integration/testsuite/Magento/Customer/_files/customer_confirmation_config_disable_rollback.php b/dev/tests/integration/testsuite/Magento/Customer/_files/customer_confirmation_config_disable_rollback.php new file mode 100644 index 0000000000000..36743b4a20e9a --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Customer/_files/customer_confirmation_config_disable_rollback.php @@ -0,0 +1,23 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +declare(strict_types=1); + +use Magento\Config\Model\ResourceModel\Config; +use Magento\Framework\Registry; +use Magento\TestFramework\Helper\Bootstrap; + +/** @var Registry $registry */ +$registry = Bootstrap::getObjectManager()->get(Registry::class); +$registry->unregister('isSecureArea'); +$registry->register('isSecureArea', true); + +/** @var Config $config */ +$config = Bootstrap::getObjectManager()->create(Config::class); +$config->deleteConfig('customer/create_account/confirm'); + +$registry->unregister('isSecureArea'); +$registry->register('isSecureArea', false); diff --git a/dev/tests/integration/testsuite/Magento/Customer/_files/customer_confirmation_config_enable.php b/dev/tests/integration/testsuite/Magento/Customer/_files/customer_confirmation_config_enable.php new file mode 100644 index 0000000000000..c8deb7ec2a536 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Customer/_files/customer_confirmation_config_enable.php @@ -0,0 +1,21 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +declare(strict_types=1); + +use Magento\Store\Model\ScopeInterface; +use Magento\Framework\App\Config\MutableScopeConfigInterface; +use Magento\TestFramework\Helper\Bootstrap; + +$objectManager = Bootstrap::getObjectManager(); +$mutableScopeConfig = $objectManager->create(MutableScopeConfigInterface::class); + +$mutableScopeConfig->setValue( + 'customer/create_account/confirm', + 1, + ScopeInterface::SCOPE_WEBSITES, + null +); diff --git a/dev/tests/integration/testsuite/Magento/Customer/_files/customer_confirmation_config_enable_rollback.php b/dev/tests/integration/testsuite/Magento/Customer/_files/customer_confirmation_config_enable_rollback.php new file mode 100644 index 0000000000000..36743b4a20e9a --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Customer/_files/customer_confirmation_config_enable_rollback.php @@ -0,0 +1,23 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +declare(strict_types=1); + +use Magento\Config\Model\ResourceModel\Config; +use Magento\Framework\Registry; +use Magento\TestFramework\Helper\Bootstrap; + +/** @var Registry $registry */ +$registry = Bootstrap::getObjectManager()->get(Registry::class); +$registry->unregister('isSecureArea'); +$registry->register('isSecureArea', true); + +/** @var Config $config */ +$config = Bootstrap::getObjectManager()->create(Config::class); +$config->deleteConfig('customer/create_account/confirm'); + +$registry->unregister('isSecureArea'); +$registry->register('isSecureArea', false); diff --git a/dev/tests/integration/testsuite/Magento/Customer/_files/customer_confirmation_email_address_with_special_chars.php b/dev/tests/integration/testsuite/Magento/Customer/_files/customer_confirmation_email_address_with_special_chars.php new file mode 100644 index 0000000000000..c4f046bac57a6 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Customer/_files/customer_confirmation_email_address_with_special_chars.php @@ -0,0 +1,37 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +declare(strict_types=1); + +use Magento\Customer\Api\CustomerRepositoryInterface; +use Magento\Customer\Api\Data\CustomerInterface; +use Magento\Customer\Model\Customer; +use Magento\TestFramework\Helper\Bootstrap; + +include __DIR__ . '/customer_confirmation_config_enable.php'; + +$objectManager = Bootstrap::getObjectManager(); + +/** @var Customer $customer */ +$customer = $objectManager->create(Customer::class); +/** @var CustomerRepositoryInterface $customerRepository */ +$customerRepository = $objectManager->create(CustomerRepositoryInterface::class); +/** @var CustomerInterface $customerInterface */ +$customerInterface = $objectManager->create(CustomerInterface::class); + +$customerInterface->setWebsiteId(1) + ->setEmail('customer+confirmation@example.com') + ->setConfirmation($customer->getRandomConfirmationKey()) + ->setGroupId(1) + ->setStoreId(1) + ->setFirstname('John') + ->setLastname('Smith') + ->setDefaultBilling(1) + ->setDefaultShipping(1) + ->setTaxvat('12') + ->setGender(0); + +$customerRepository->save($customerInterface, 'password'); diff --git a/dev/tests/integration/testsuite/Magento/Customer/_files/customer_confirmation_email_address_with_special_chars_rollback.php b/dev/tests/integration/testsuite/Magento/Customer/_files/customer_confirmation_email_address_with_special_chars_rollback.php new file mode 100644 index 0000000000000..7a0ebf74ed8a0 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Customer/_files/customer_confirmation_email_address_with_special_chars_rollback.php @@ -0,0 +1,31 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +declare(strict_types=1); + +include __DIR__ . '/customer_confirmation_config_enable_rollback.php'; + +use Magento\Customer\Api\CustomerRepositoryInterface; +use Magento\Framework\Registry; +use Magento\TestFramework\Helper\Bootstrap; + +/** @var Registry $registry */ +$registry = Bootstrap::getObjectManager()->get(Registry::class); +$registry->unregister('isSecureArea'); +$registry->register('isSecureArea', true); + +/** @var CustomerRepositoryInterface $customerRepository */ +$customerRepository = Bootstrap::getObjectManager()->create(CustomerRepositoryInterface::class); + +try { + $customer = $customerRepository->get('customer+confirmation@example.com'); + $customerRepository->delete($customer); +} catch (\Magento\Framework\Exception\NoSuchEntityException $e) { + // Customer with the specified email does not exist +} + +$registry->unregister('isSecureArea'); +$registry->register('isSecureArea', false); diff --git a/dev/tests/integration/testsuite/Magento/Email/Model/Template/FilterTest.php b/dev/tests/integration/testsuite/Magento/Email/Model/Template/FilterTest.php index a68b0942ec090..dd55dcc8b47c7 100644 --- a/dev/tests/integration/testsuite/Magento/Email/Model/Template/FilterTest.php +++ b/dev/tests/integration/testsuite/Magento/Email/Model/Template/FilterTest.php @@ -130,22 +130,22 @@ public function layoutDirectiveDataProvider() 'area parameter - omitted' => [ 'adminhtml', 'handle="email_template_test_handle"', - '<b>Email content for frontend/Magento/default theme</b>', + '<strong>Email content for frontend/Magento/default theme</strong>', ], 'area parameter - frontend' => [ 'adminhtml', 'handle="email_template_test_handle" area="frontend"', - '<b>Email content for frontend/Magento/default theme</b>', + '<strong>Email content for frontend/Magento/default theme</strong>', ], 'area parameter - backend' => [ 'frontend', 'handle="email_template_test_handle" area="adminhtml"', - '<b>Email content for adminhtml/Magento/default theme</b>', + '<strong>Email content for adminhtml/Magento/default theme</strong>', ], 'custom parameter' => [ 'frontend', 'handle="email_template_test_handle" template="Magento_Email::sample_email_content_custom.phtml"', - '<b>Custom Email content for frontend/Magento/default theme</b>', + '<strong>Custom Email content for frontend/Magento/default theme</strong>', ], ]; return $result; diff --git a/dev/tests/integration/testsuite/Magento/Email/Model/TemplateTest.php b/dev/tests/integration/testsuite/Magento/Email/Model/TemplateTest.php index a83de07443e95..7789a79794f39 100644 --- a/dev/tests/integration/testsuite/Magento/Email/Model/TemplateTest.php +++ b/dev/tests/integration/testsuite/Magento/Email/Model/TemplateTest.php @@ -315,25 +315,25 @@ public function templateDirectiveDataProvider() Area::AREA_FRONTEND, TemplateTypesInterface::TYPE_HTML, '{{template config_path="customer/create_account/email_template"}}', - '<b>customer_create_account_email_template template from Vendor/custom_theme</b>', + '<strong>customer_create_account_email_template template from Vendor/custom_theme</strong>', ], 'Template from parent theme - frontend' => [ Area::AREA_FRONTEND, TemplateTypesInterface::TYPE_HTML, '{{template config_path="customer/create_account/email_confirmation_template"}}', - '<b>customer_create_account_email_confirmation_template template from Vendor/default</b>', + '<strong>customer_create_account_email_confirmation_template template from Vendor/default</strong', ], 'Template from grandparent theme - frontend' => [ Area::AREA_FRONTEND, TemplateTypesInterface::TYPE_HTML, '{{template config_path="customer/create_account/email_confirmed_template"}}', - '<b>customer_create_account_email_confirmed_template template from Magento/default</b>', + '<strong>customer_create_account_email_confirmed_template template from Magento/default</strong', ], 'Template from grandparent theme - adminhtml' => [ BackendFrontNameResolver::AREA_CODE, TemplateTypesInterface::TYPE_HTML, '{{template config_path="catalog/productalert_cron/error_email_template"}}', - '<b>catalog_productalert_cron_error_email_template template from Magento/default</b>', + '<strong>catalog_productalert_cron_error_email_template template from Magento/default</strong', null, null, true, diff --git a/dev/tests/integration/testsuite/Magento/Email/Model/_files/design/adminhtml/Magento/default/Magento_Email/templates/sample_email_content.phtml b/dev/tests/integration/testsuite/Magento/Email/Model/_files/design/adminhtml/Magento/default/Magento_Email/templates/sample_email_content.phtml index d53468a38376f..bb0073cedee96 100644 --- a/dev/tests/integration/testsuite/Magento/Email/Model/_files/design/adminhtml/Magento/default/Magento_Email/templates/sample_email_content.phtml +++ b/dev/tests/integration/testsuite/Magento/Email/Model/_files/design/adminhtml/Magento/default/Magento_Email/templates/sample_email_content.phtml @@ -4,4 +4,4 @@ * See COPYING.txt for license details. */ ?> -<b>Email content for adminhtml/Magento/default theme</b> +<strong>Email content for adminhtml/Magento/default theme</strong> diff --git a/dev/tests/integration/testsuite/Magento/Email/Model/_files/design/adminhtml/Magento/default/Magento_ProductAlert/email/cron_error.html b/dev/tests/integration/testsuite/Magento/Email/Model/_files/design/adminhtml/Magento/default/Magento_ProductAlert/email/cron_error.html index f13e54edf93a4..d65f9d4c40877 100644 --- a/dev/tests/integration/testsuite/Magento/Email/Model/_files/design/adminhtml/Magento/default/Magento_ProductAlert/email/cron_error.html +++ b/dev/tests/integration/testsuite/Magento/Email/Model/_files/design/adminhtml/Magento/default/Magento_ProductAlert/email/cron_error.html @@ -4,4 +4,4 @@ * See COPYING.txt for license details. */ --> -<b>catalog_productalert_cron_error_email_template template from Magento/default</b> +<strong>catalog_productalert_cron_error_email_template template from Magento/default</strong> diff --git a/dev/tests/integration/testsuite/Magento/Email/Model/_files/design/frontend/Magento/default/Magento_Customer/email/account_new_confirmed.html b/dev/tests/integration/testsuite/Magento/Email/Model/_files/design/frontend/Magento/default/Magento_Customer/email/account_new_confirmed.html index f687fc041db1e..ffc4d8893fe98 100644 --- a/dev/tests/integration/testsuite/Magento/Email/Model/_files/design/frontend/Magento/default/Magento_Customer/email/account_new_confirmed.html +++ b/dev/tests/integration/testsuite/Magento/Email/Model/_files/design/frontend/Magento/default/Magento_Customer/email/account_new_confirmed.html @@ -4,4 +4,4 @@ * See COPYING.txt for license details. */ --> -<b>customer_create_account_email_confirmed_template template from Magento/default</b> +<strong>customer_create_account_email_confirmed_template template from Magento/default</strong> diff --git a/dev/tests/integration/testsuite/Magento/Email/Model/_files/design/frontend/Magento/default/Magento_Email/templates/sample_email_content.phtml b/dev/tests/integration/testsuite/Magento/Email/Model/_files/design/frontend/Magento/default/Magento_Email/templates/sample_email_content.phtml index acbdf16d474df..9c973818272c8 100644 --- a/dev/tests/integration/testsuite/Magento/Email/Model/_files/design/frontend/Magento/default/Magento_Email/templates/sample_email_content.phtml +++ b/dev/tests/integration/testsuite/Magento/Email/Model/_files/design/frontend/Magento/default/Magento_Email/templates/sample_email_content.phtml @@ -4,4 +4,4 @@ * See COPYING.txt for license details. */ ?> -<b>Email content for frontend/Magento/default theme</b> +<strong>Email content for frontend/Magento/default theme</strong> diff --git a/dev/tests/integration/testsuite/Magento/Email/Model/_files/design/frontend/Magento/default/Magento_Email/templates/sample_email_content_custom.phtml b/dev/tests/integration/testsuite/Magento/Email/Model/_files/design/frontend/Magento/default/Magento_Email/templates/sample_email_content_custom.phtml index 1730bf904bb34..4ed5685ee0106 100644 --- a/dev/tests/integration/testsuite/Magento/Email/Model/_files/design/frontend/Magento/default/Magento_Email/templates/sample_email_content_custom.phtml +++ b/dev/tests/integration/testsuite/Magento/Email/Model/_files/design/frontend/Magento/default/Magento_Email/templates/sample_email_content_custom.phtml @@ -4,4 +4,4 @@ * See COPYING.txt for license details. */ ?> -<b>Custom Email content for frontend/Magento/default theme</b> +<strong>Custom Email content for frontend/Magento/default theme</strong> diff --git a/dev/tests/integration/testsuite/Magento/Email/Model/_files/design/frontend/Vendor/custom_theme/Magento_Customer/email/account_new.html b/dev/tests/integration/testsuite/Magento/Email/Model/_files/design/frontend/Vendor/custom_theme/Magento_Customer/email/account_new.html index 7e8f9bd1b12b6..46257060f8284 100644 --- a/dev/tests/integration/testsuite/Magento/Email/Model/_files/design/frontend/Vendor/custom_theme/Magento_Customer/email/account_new.html +++ b/dev/tests/integration/testsuite/Magento/Email/Model/_files/design/frontend/Vendor/custom_theme/Magento_Customer/email/account_new.html @@ -4,4 +4,4 @@ * See COPYING.txt for license details. */ --> -<b>customer_create_account_email_template template from Vendor/custom_theme</b> +<strong>customer_create_account_email_template template from Vendor/custom_theme</strong> diff --git a/dev/tests/integration/testsuite/Magento/Email/Model/_files/design/frontend/Vendor/default/Magento_Customer/email/account_new_confirmation.html b/dev/tests/integration/testsuite/Magento/Email/Model/_files/design/frontend/Vendor/default/Magento_Customer/email/account_new_confirmation.html index c5801b6557a61..9c52c5a1b38cf 100644 --- a/dev/tests/integration/testsuite/Magento/Email/Model/_files/design/frontend/Vendor/default/Magento_Customer/email/account_new_confirmation.html +++ b/dev/tests/integration/testsuite/Magento/Email/Model/_files/design/frontend/Vendor/default/Magento_Customer/email/account_new_confirmation.html @@ -4,4 +4,4 @@ * See COPYING.txt for license details. */ --> -<b>customer_create_account_email_confirmation_template template from Vendor/default</b> +<strong>customer_create_account_email_confirmation_template template from Vendor/default</strong> diff --git a/dev/tests/integration/testsuite/Magento/Newsletter/Controller/SubscriberTest.php b/dev/tests/integration/testsuite/Magento/Newsletter/Controller/SubscriberTest.php index 5347881f5e7d4..e13af108e6fa5 100644 --- a/dev/tests/integration/testsuite/Magento/Newsletter/Controller/SubscriberTest.php +++ b/dev/tests/integration/testsuite/Magento/Newsletter/Controller/SubscriberTest.php @@ -53,11 +53,10 @@ public function testNewActionUsedEmail() $this->getRequest()->setPostValue([ 'email' => 'customer@example.com', ]); - $this->dispatch('newsletter/subscriber/new'); $this->assertSessionMessages($this->equalTo([ - 'There was a problem with the subscription: This email address is already assigned to another user.', + 'Thank you for your subscription.', ])); $this->assertRedirect($this->anything()); } diff --git a/dev/tests/integration/testsuite/Magento/Newsletter/Model/SubscriberTest.php b/dev/tests/integration/testsuite/Magento/Newsletter/Model/SubscriberTest.php index c6ba498319ff8..6c7aa204fded2 100644 --- a/dev/tests/integration/testsuite/Magento/Newsletter/Model/SubscriberTest.php +++ b/dev/tests/integration/testsuite/Magento/Newsletter/Model/SubscriberTest.php @@ -8,6 +8,11 @@ class SubscriberTest extends \PHPUnit\Framework\TestCase { + /** + * @var \Magento\TestFramework\ObjectManager + */ + private $objectManager; + /** * @var Subscriber */ @@ -15,7 +20,8 @@ class SubscriberTest extends \PHPUnit\Framework\TestCase protected function setUp() { - $this->_model = \Magento\TestFramework\Helper\Bootstrap::getObjectManager()->create( + $this->objectManager = \Magento\TestFramework\Helper\Bootstrap::getObjectManager(); + $this->_model = $this->objectManager->create( \Magento\Newsletter\Model\Subscriber::class ); } @@ -27,7 +33,7 @@ protected function setUp() public function testEmailConfirmation() { $this->_model->subscribe('customer_confirm@example.com'); - $transportBuilder = \Magento\TestFramework\Helper\Bootstrap::getObjectManager() + $transportBuilder = $this->objectManager ->get(\Magento\TestFramework\Mail\Template\TransportBuilderMock::class); // confirmationCode 'ysayquyajua23iq29gxwu2eax2qb6gvy' is taken from fixture $this->assertContains( @@ -77,4 +83,47 @@ public function testUnsubscribeSubscribeByCustomerId() $this->assertSame($this->_model, $this->_model->subscribeCustomerById(1)); $this->assertEquals(Subscriber::STATUS_SUBSCRIBED, $this->_model->getSubscriberStatus()); } + + /** + * @magentoDataFixture Magento/Store/_files/second_store.php + */ + public function testSubscribeGuestRegisterCustomerWithSameEmailFromDifferentStore() + { + /** @var \Magento\Store\Model\Store $store */ + $store = $this->objectManager->create(\Magento\Store\Model\Store::class); + $defaultStoreId = $store->load('default', 'code')->getId(); + $secondStoreId = $store->load('fixture_second_store', 'code')->getId(); + + $email = 'test@example.com'; + + // Subscribing guest to a newsletter from default store + $this->_model->setStoreId($defaultStoreId) + ->setCustomerId(0) + ->setSubscriberEmail($email) + ->setSubscriberStatus(\Magento\Newsletter\Model\Subscriber::STATUS_SUBSCRIBED) + ->save(); + + // Registering customer with the same email from second store + $customer = $this->objectManager->create( + \Magento\Customer\Model\Data\Customer::class, + [ + 'data' => [ + \Magento\Customer\Model\Data\Customer::FIRSTNAME => 'John', + \Magento\Customer\Model\Data\Customer::LASTNAME => 'Doe', + \Magento\Customer\Model\Data\Customer::EMAIL => $email, + \Magento\Customer\Model\Data\Customer::STORE_ID => $secondStoreId, + ] + ] + ); + + /** @var \Magento\Customer\Api\AccountManagementInterface $accountManagement */ + $accountManagement = $this->objectManager->get(\Magento\Customer\Api\AccountManagementInterface::class); + $customer = $accountManagement->createAccount($customer); + + /** @var \Magento\Newsletter\Model\ResourceModel\Subscriber\Collection $subscribers */ + $subscribers = $this->_model->getCollection()->addFieldToFilter('subscriber_email', $email); + + $this->assertCount(1, $subscribers->getItems()); + $this->assertEquals($subscribers->getFirstItem()->getCustomerId(), $customer->getId()); + } } diff --git a/dev/tests/integration/testsuite/Magento/Paypal/Controller/ExpressTest.php b/dev/tests/integration/testsuite/Magento/Paypal/Controller/ExpressTest.php index 95e3abbfe6ff1..d10befc3a811c 100644 --- a/dev/tests/integration/testsuite/Magento/Paypal/Controller/ExpressTest.php +++ b/dev/tests/integration/testsuite/Magento/Paypal/Controller/ExpressTest.php @@ -226,4 +226,42 @@ public function testReturnAction() $this->_objectManager->removeSharedInstance(ApiFactory::class); $this->_objectManager->removeSharedInstance(PaypalSession::class); } + + /** + * @magentoConfigFixture current_store carriers/freeshipping/active 1 + * @magentoDataFixture Magento/Sales/_files/quote.php + * @magentoDataFixture Magento/Paypal/_files/quote_payment.php + * @return void + */ + public function testPlaceOrderZeroGrandTotal() + { + /** @var Quote $quote */ + $quote = $this->_objectManager->create(Quote::class); + $quote->load('test01', 'reserved_order_id'); + $quote->getShippingAddress()->setShippingMethod('freeshipping'); + $quote->getShippingAddress()->setCollectShippingRates(true); + /** @var \Magento\Quote\Model\Quote\Item[] $items */ + $items = $quote->getItemsCollection()->getItems(); + $quoteItem = reset($items); + /** @var \Magento\Quote\Model\Quote\Item\Updater $quoteItemUpdater */ + $quoteItemUpdater = $this->_objectManager->get(\Magento\Quote\Model\Quote\Item\Updater::class); + $quoteItemUpdater->update($quoteItem, ['qty' => 1, 'custom_price' => 0]); + $quote->setTotalsCollectedFlag(false)->collectTotals()->save(); + + $this->_objectManager->get(Session::class)->setQuoteId($quote->getId()); + + $this->dispatch('paypal/express/placeOrder'); + $this->assertSessionMessages( + $this->equalTo( + [ + htmlspecialchars( + (string)__('PayPal can\'t process orders with a zero balance due. ' + . 'To finish your purchase, please go through the standard checkout process.'), + ENT_QUOTES + ) + ] + ), + \Magento\Framework\Message\MessageInterface::TYPE_ERROR + ); + } } diff --git a/dev/tests/integration/testsuite/Magento/Paypal/Model/IpnTest.php b/dev/tests/integration/testsuite/Magento/Paypal/Model/IpnTest.php index 6b92338e9932a..3c126bcdc2c5b 100644 --- a/dev/tests/integration/testsuite/Magento/Paypal/Model/IpnTest.php +++ b/dev/tests/integration/testsuite/Magento/Paypal/Model/IpnTest.php @@ -64,7 +64,7 @@ public function testProcessIpnRequestFullRefund() $creditmemoItems = $order->getCreditmemosCollection()->getItems(); $creditmemo = current($creditmemoItems); - $this->assertEquals(Order::STATE_PROCESSING, $order->getState()); + $this->assertEquals(Order::STATE_CLOSED, $order->getState()); $this->assertEquals(1, count($creditmemoItems)); $this->assertEquals(Creditmemo::STATE_REFUNDED, $creditmemo->getState()); $this->assertEquals(10, $order->getSubtotalRefunded()); @@ -146,7 +146,7 @@ public function testProcessIpnRequestRestRefund() $creditmemoItems = $order->getCreditmemosCollection()->getItems(); - $this->assertEquals(Order::STATE_PROCESSING, $order->getState()); + $this->assertEquals(Order::STATE_CLOSED, $order->getState()); $this->assertEquals(1, count($creditmemoItems)); $this->assertEquals(10, $order->getSubtotalRefunded()); $this->assertEquals(10, $order->getBaseSubtotalRefunded()); diff --git a/dev/tests/integration/testsuite/Magento/Persistent/Block/Header/AdditionalTest.php b/dev/tests/integration/testsuite/Magento/Persistent/Block/Header/AdditionalTest.php index fba5354b691d6..2e447e7854a7c 100644 --- a/dev/tests/integration/testsuite/Magento/Persistent/Block/Header/AdditionalTest.php +++ b/dev/tests/integration/testsuite/Magento/Persistent/Block/Header/AdditionalTest.php @@ -56,8 +56,8 @@ public function testToHtml() $this->_customerSession->loginById(1); $translation = __('Not you?'); - $this->assertStringMatchesFormat( - '%A<span>%A<a%Ahref="' . $this->_block->getHref() . '"%A>' . $translation . '</a>%A</span>%A', + $this->assertContains( + '<a href="' . $this->_block->getHref() . '">' . $translation . '</a>', $this->_block->toHtml() ); $this->_customerSession->logout(); diff --git a/dev/tests/integration/testsuite/Magento/Persistent/Model/ObserverTest.php b/dev/tests/integration/testsuite/Magento/Persistent/Model/ObserverTest.php index ecf2cd77a13ff..d0c253fc7a64b 100644 --- a/dev/tests/integration/testsuite/Magento/Persistent/Model/ObserverTest.php +++ b/dev/tests/integration/testsuite/Magento/Persistent/Model/ObserverTest.php @@ -9,7 +9,6 @@ use Magento\Customer\Model\Context; /** - * @magentoDataFixture Magento/Persistent/_files/persistent.php * @SuppressWarnings(PHPMD.CouplingBetweenObjects) */ class ObserverTest extends \PHPUnit\Framework\TestCase @@ -29,11 +28,6 @@ class ObserverTest extends \PHPUnit\Framework\TestCase */ protected $customerRepository; - /** - * @var \Magento\Persistent\Helper\Session - */ - protected $_persistentSessionHelper; - /** * @var \Magento\Framework\ObjectManagerInterface */ @@ -44,11 +38,6 @@ class ObserverTest extends \PHPUnit\Framework\TestCase */ protected $_observer; - /** - * @var \Magento\Customer\Model\Session - */ - protected $_customerSession; - /** * @var \Magento\Checkout\Model\Session | \PHPUnit_Framework_MockObject_MockObject */ @@ -58,8 +47,6 @@ public function setUp() { $this->_objectManager = \Magento\TestFramework\Helper\Bootstrap::getObjectManager(); - $this->_customerSession = $this->_objectManager->get(\Magento\Customer\Model\Session::class); - $this->_customerViewHelper = $this->_objectManager->create( \Magento\Customer\Helper\View::class ); @@ -75,8 +62,6 @@ public function setUp() \Magento\Checkout\Model\Session::class )->disableOriginalConstructor()->setMethods([])->getMock(); - $this->_persistentSessionHelper = $this->_objectManager->create(\Magento\Persistent\Helper\Session::class); - $this->_observer = $this->_objectManager->create( \Magento\Persistent\Model\Observer::class, [ @@ -89,16 +74,11 @@ public function setUp() } /** - * @magentoConfigFixture current_store persistent/options/enabled 1 - * @magentoConfigFixture current_store persistent/options/remember_enabled 1 - * @magentoConfigFixture current_store persistent/options/remember_default 1 * @magentoAppArea frontend * @magentoAppIsolation enabled */ public function testEmulateWelcomeBlock() { - $this->_customerSession->loginById(1); - $httpContext = new \Magento\Framework\App\Http\Context(); $httpContext->setValue(Context::CONTEXT_AUTH, 1, 1); $block = $this->_objectManager->create( @@ -108,15 +88,7 @@ public function testEmulateWelcomeBlock() ] ); $this->_observer->emulateWelcomeBlock($block); - $customerName = $this->_escaper->escapeHtml( - $this->_customerViewHelper->getCustomerName( - $this->customerRepository->getById( - $this->_persistentSessionHelper->getSession()->getCustomerId() - ) - ) - ); - $translation = __('Welcome, %1!', $customerName)->__toString(); - $this->assertStringMatchesFormat('%A' . $translation . '%A', $block->getWelcome()->__toString()); - $this->_customerSession->logout(); + + $this->assertEquals(' ', $block->getWelcome()); } } diff --git a/dev/tests/integration/testsuite/Magento/Quote/Model/Quote/Item/UpdaterTest.php b/dev/tests/integration/testsuite/Magento/Quote/Model/Quote/Item/UpdaterTest.php new file mode 100644 index 0000000000000..137d3347653bc --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Quote/Model/Quote/Item/UpdaterTest.php @@ -0,0 +1,60 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +namespace Magento\Quote\Model\Quote\Item; + +use Magento\Quote\Api\CartItemRepositoryInterface; +use Magento\Quote\Api\Data\CartItemInterface; +use Magento\Quote\Model\Quote; +use Magento\TestFramework\Helper\Bootstrap; +use Magento\Quote\Model\Quote\Item\Updater; + +/** + * Tests \Magento\Quote\Model\Quote\Item\Updater + */ +class UpdaterTest extends \PHPUnit\Framework\TestCase +{ + /** + * @var Updater + */ + private $updater; + + /** + * @inheritdoc + */ + protected function setUp() + { + parent::setUp(); + $this->updater = Bootstrap::getObjectManager()->create(Updater::class); + } + + /** + * @magentoDataFixture Magento/Sales/_files/quote_with_custom_price.php + * @return void + */ + public function testUpdate() + { + /** @var CartItemRepositoryInterface $quoteItemRepository */ + $quoteItemRepository = Bootstrap::getObjectManager()->create(CartItemRepositoryInterface::class); + /** @var Quote $quote */ + $quote = Bootstrap::getObjectManager()->create(Quote::class); + $quoteId = $quote->load('test01', 'reserved_order_id')->getId(); + /** @var CartItemInterface[] $quoteItems */ + $quoteItems = $quoteItemRepository->getList($quoteId); + /** @var CartItemInterface $actualQuoteItem */ + $actualQuoteItem = array_pop($quoteItems); + $this->assertInstanceOf(CartItemInterface::class, $actualQuoteItem); + + $info = [ + 'qty' => 1, + ]; + $this->updater->update($actualQuoteItem, $info); + + $this->assertNull( + $actualQuoteItem->getCustomPrice(), + 'Item custom price has to be null' + ); + } +} diff --git a/dev/tests/integration/testsuite/Magento/Review/Controller/CaseCheckAddingProductReviewTest.php b/dev/tests/integration/testsuite/Magento/Review/Controller/CaseCheckAddingProductReviewTest.php new file mode 100644 index 0000000000000..49e980ed53602 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Review/Controller/CaseCheckAddingProductReviewTest.php @@ -0,0 +1,120 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +namespace Magento\Review\Controller; + +use Magento\Catalog\Api\Data\ProductInterface; +use Magento\Catalog\Api\ProductRepositoryInterface; +use Magento\Framework\Data\Form\FormKey; +use Magento\Framework\Message\MessageInterface; +use Magento\TestFramework\Request; +use Magento\TestFramework\TestCase\AbstractController; + +/** + * Test review product controller behavior + * + * @magentoAppArea frontend + */ +class CaseCheckAddingProductReviewTest extends AbstractController +{ + /** + * Test adding a review for allowed guests with incomplete data by a not logged in user + * + * @magentoDbIsolation enabled + * @magentoAppIsolation enabled + * @magentoDataFixture Magento/Review/_files/config.php + * @magentoDataFixture Magento/Catalog/_files/products.php + */ + public function testAttemptForGuestToAddReviewsWithIncompleteData() + { + $product = $this->getProduct(); + /** @var FormKey $formKey */ + $formKey = $this->_objectManager->get(FormKey::class); + $post = [ + 'nickname' => 'Test nick', + 'title' => 'Summary', + 'form_key' => $formKey->getFormKey(), + ]; + $this->prepareRequestData($post); + $this->dispatch('review/product/post/id/' . $product->getId()); + $this->assertSessionMessages( + $this->equalTo(['Please enter a review.']), + MessageInterface::TYPE_ERROR + ); + } + + /** + * Test adding a review for not allowed guests by a guest + * + * @magentoDbIsolation enabled + * @magentoAppIsolation enabled + * @magentoDataFixture Magento/Review/_files/disable_config.php + * @magentoDataFixture Magento/Catalog/_files/products.php + */ + public function testAttemptForGuestToAddReview() + { + $product = $this->getProduct(); + /** @var FormKey $formKey */ + $formKey = $this->_objectManager->get(FormKey::class); + $post = [ + 'nickname' => 'Test nick', + 'title' => 'Summary', + 'detail' => 'Test Details', + 'form_key' => $formKey->getFormKey(), + ]; + + $this->prepareRequestData($post); + $this->dispatch('review/product/post/id/' . $product->getId()); + + $this->assertRedirect($this->stringContains('customer/account/login')); + } + + /** + * Test successfully adding a product review by a guest + * + * @magentoDbIsolation enabled + * @magentoAppIsolation enabled + * @magentoDataFixture Magento/Review/_files/config.php + * @magentoDataFixture Magento/Catalog/_files/products.php + */ + public function testSuccessfullyAddingProductReviewForGuest() + { + $product = $this->getProduct(); + /** @var FormKey $formKey */ + $formKey = $this->_objectManager->get(FormKey::class); + $post = [ + 'nickname' => 'Test nick', + 'title' => 'Summary', + 'detail' => 'Test Details', + 'form_key' => $formKey->getFormKey(), + ]; + + $this->prepareRequestData($post); + $this->dispatch('review/product/post/id/' . $product->getId()); + + $this->assertSessionMessages( + $this->equalTo(['You submitted your review for moderation.']), + MessageInterface::TYPE_SUCCESS + ); + } + + /** + * @return ProductInterface + */ + private function getProduct() + { + return $this->_objectManager->get(ProductRepositoryInterface::class)->get('custom-design-simple-product'); + } + + /** + * @param array $postData + * @return void + */ + private function prepareRequestData($postData) + { + $this->getRequest()->setMethod(Request::METHOD_POST); + $this->getRequest()->setPostValue($postData); + } +} diff --git a/dev/tests/integration/testsuite/Magento/Review/_files/disable_config.php b/dev/tests/integration/testsuite/Magento/Review/_files/disable_config.php new file mode 100644 index 0000000000000..ee21150bd6129 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Review/_files/disable_config.php @@ -0,0 +1,15 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +/** @var Value $config */ +use Magento\Framework\App\Config\Value; + +$config = \Magento\TestFramework\Helper\Bootstrap::getObjectManager()->create(Value::class); +$config->setPath('catalog/review/allow_guest'); +$config->setScope('default'); +$config->setScopeId(0); +$config->setValue(0); +$config->save(); diff --git a/dev/tests/integration/testsuite/Magento/Sales/Block/Adminhtml/Order/Create/Form/AccountTest.php b/dev/tests/integration/testsuite/Magento/Sales/Block/Adminhtml/Order/Create/Form/AccountTest.php index 999522a49e006..ce3f3a3e1fc8e 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 @@ -11,26 +11,37 @@ namespace Magento\Sales\Block\Adminhtml\Order\Create\Form; use Magento\Backend\Model\Session\Quote as SessionQuote; +use Magento\Customer\Api\Data\AttributeMetadataInterface; use Magento\Customer\Api\Data\AttributeMetadataInterfaceFactory; +use Magento\Customer\Model\Data\Option; use Magento\Customer\Model\Metadata\Form; use Magento\Customer\Model\Metadata\FormFactory; use Magento\Framework\View\LayoutInterface; use Magento\Quote\Model\Quote; use Magento\TestFramework\Helper\Bootstrap; +use Magento\TestFramework\ObjectManager; +use PHPUnit\Framework\MockObject\MockObject; /** * @magentoAppArea adminhtml */ class AccountTest extends \PHPUnit\Framework\TestCase { - /** @var Account */ + /** + * @var Account + */ private $accountBlock; /** - * @var Bootstrap + * @var ObjectManager */ private $objectManager; + /** + * @var SessionQuote|MockObject + */ + private $session; + /** * @magentoDataFixture Magento/Sales/_files/quote.php */ @@ -38,19 +49,23 @@ protected function setUp() { $this->objectManager = Bootstrap::getObjectManager(); $quote = $this->objectManager->create(Quote::class)->load(1); - $sessionQuoteMock = $this->getMockBuilder( - SessionQuote::class - )->disableOriginalConstructor()->setMethods( - ['getCustomerId', 'getStore', 'getStoreId', 'getQuote'] - )->getMock(); - $sessionQuoteMock->expects($this->any())->method('getCustomerId')->will($this->returnValue(1)); - $sessionQuoteMock->expects($this->any())->method('getQuote')->will($this->returnValue($quote)); + + $this->session = $this->getMockBuilder(SessionQuote::class) + ->disableOriginalConstructor() + ->setMethods(['getCustomerId', 'getStore', 'getStoreId', 'getQuote', 'getQuoteId']) + ->getMock(); + $this->session->method('getCustomerId') + ->willReturn(1); + $this->session->method('getQuote') + ->willReturn($quote); + $this->session->method('getQuoteId') + ->willReturn($quote->getId()); /** @var LayoutInterface $layout */ $layout = $this->objectManager->get(LayoutInterface::class); $this->accountBlock = $layout->createBlock( Account::class, 'address_block' . rand(), - ['sessionQuote' => $sessionQuoteMock] + ['sessionQuote' => $this->session] ); parent::setUp(); } @@ -62,13 +77,13 @@ public function testGetForm() { $expectedFields = ['group_id', 'email']; $form = $this->accountBlock->getForm(); - $this->assertEquals(1, $form->getElements()->count(), "Form has invalid number of fieldsets"); + self::assertEquals(1, $form->getElements()->count(), "Form has invalid number of fieldsets"); $fieldset = $form->getElements()[0]; - $this->assertEquals(count($expectedFields), $fieldset->getElements()->count()); + self::assertEquals(count($expectedFields), $fieldset->getElements()->count()); foreach ($fieldset->getElements() as $element) { - $this->assertTrue( + self::assertTrue( in_array($element->getId(), $expectedFields), sprintf('Unexpected field "%s" in form.', $element->getId()) ); @@ -79,6 +94,7 @@ public function testGetForm() * Tests a case when user defined custom attribute has default value. * * @magentoDataFixture Magento/Customer/_files/customer.php + * @magentoConfigFixture current_store customer/create_account/default_group 3 */ public function testGetFormWithUserDefinedAttribute() { @@ -91,18 +107,27 @@ public function testGetFormWithUserDefinedAttribute() $form = $accountBlock->getForm(); $form->setUseContainer(true); + $content = $form->toHtml(); - $this->assertContains( + self::assertContains( '<option value="1" selected="selected">Yes</option>', - $form->toHtml(), - 'Default value for user defined custom attribute should be selected' + $content, + 'Default value for user defined custom attribute should be selected.' + ); + + self::assertContains( + '<option value="3" selected="selected">Customer Group 1</option>', + $content, + 'The Customer Group specified for the chosen store should be selected.' ); } /** - * @return \PHPUnit_Framework_MockObject_MockObject + * Creates a mock for Form object. + * + * @return MockObject */ - private function getFormFactoryMock(): \PHPUnit_Framework_MockObject_MockObject + private function getFormFactoryMock() { /** @var AttributeMetadataInterfaceFactory $attributeMetadataFactory */ $attributeMetadataFactory = $this->objectManager->create(AttributeMetadataInterfaceFactory::class); @@ -113,11 +138,12 @@ private function getFormFactoryMock(): \PHPUnit_Framework_MockObject_MockObject ->setDefaultValue('1') ->setFrontendLabel('Yes/No'); + /** @var Form|MockObject $form */ $form = $this->getMockBuilder(Form::class) ->disableOriginalConstructor() ->getMock(); $form->method('getUserAttributes')->willReturn([$booleanAttribute]); - $form->method('getSystemAttributes')->willReturn([]); + $form->method('getSystemAttributes')->willReturn([$this->createCustomerGroupAttribute()]); $formFactory = $this->getMockBuilder(FormFactory::class) ->disableOriginalConstructor() @@ -126,4 +152,33 @@ private function getFormFactoryMock(): \PHPUnit_Framework_MockObject_MockObject return $formFactory; } + + /** + * Creates a customer group attribute object. + * + * @return AttributeMetadataInterface + */ + private function createCustomerGroupAttribute(): AttributeMetadataInterface + { + /** @var Option $option1 */ + $option1 = $this->objectManager->create(Option::class); + $option1->setValue(3); + $option1->setLabel('Customer Group 1'); + + /** @var Option $option2 */ + $option2 = $this->objectManager->create(Option::class); + $option2->setValue(4); + $option2->setLabel('Customer Group 2'); + + /** @var AttributeMetadataInterfaceFactory $attributeMetadataFactory */ + $attributeMetadataFactory = $this->objectManager->create(AttributeMetadataInterfaceFactory::class); + $attribute = $attributeMetadataFactory->create() + ->setAttributeCode('group_id') + ->setBackendType('static') + ->setFrontendInput('select') + ->setOptions([$option1, $option2]) + ->setIsRequired(true); + + return $attribute; + } } diff --git a/dev/tests/integration/testsuite/Magento/Sales/Controller/Adminhtml/Order/Create/SaveTest.php b/dev/tests/integration/testsuite/Magento/Sales/Controller/Adminhtml/Order/Create/SaveTest.php index b9573c99b4493..da0f2be856c51 100644 --- a/dev/tests/integration/testsuite/Magento/Sales/Controller/Adminhtml/Order/Create/SaveTest.php +++ b/dev/tests/integration/testsuite/Magento/Sales/Controller/Adminhtml/Order/Create/SaveTest.php @@ -8,21 +8,61 @@ use Magento\Backend\Model\Session\Quote; use Magento\Customer\Api\CustomerRepositoryInterface; use Magento\Framework\Api\SearchCriteriaBuilder; +use Magento\Framework\Data\Form\FormKey; use Magento\Framework\Exception\LocalizedException; use Magento\Framework\Message\MessageInterface; use Magento\Quote\Api\CartRepositoryInterface; +use Magento\Sales\Api\Data\OrderInterface; +use Magento\Sales\Model\OrderRepository; use Magento\Sales\Model\Service\OrderService; +use Magento\TestFramework\Mail\Template\TransportBuilderMock; use Magento\TestFramework\TestCase\AbstractBackendController; +use PHPUnit\Framework\Constraint\StringContains; use PHPUnit_Framework_MockObject_MockObject as MockObject; +/** + * Class test backend order save. + * + * @magentoAppArea adminhtml + * @SuppressWarnings(PHPMD.CouplingBetweenObjects) + */ class SaveTest extends AbstractBackendController { + /** + * @var TransportBuilderMock + */ + private $transportBuilder; + + /** + * @var FormKey + */ + private $formKey; + + /** + * @var string + */ + protected $resource = 'Magento_Sales::create'; + + /** + * @var string + */ + protected $uri = 'backend/sales/order_create/save'; + + /** + * @inheritdoc + */ + protected function setUp() + { + parent::setUp(); + $this->transportBuilder = $this->_objectManager->get(TransportBuilderMock::class); + $this->formKey = $this->_objectManager->get(FormKey::class); + } + /** * Checks a case when order creation is failed on payment method processing but new customer already created * in the database and after new controller dispatching the customer should be already loaded in session * to prevent invalid validation. * - * @magentoAppArea adminhtml * @magentoDataFixture Magento/Sales/_files/quote_with_new_customer.php */ public function testExecuteWithPaymentOperation() @@ -35,7 +75,7 @@ public function testExecuteWithPaymentOperation() $email = 'john.doe001@test.com'; $data = [ 'account' => [ - 'email' => $email + 'email' => $email, ] ]; $this->getRequest()->setPostValue(['order' => $data]); @@ -64,6 +104,45 @@ public function testExecuteWithPaymentOperation() $this->_objectManager->removeSharedInstance(OrderService::class); } + /** + * @magentoDbIsolation enabled + * @magentoDataFixture Magento/Sales/_files/guest_quote_with_addresses.php + * + * @return void + */ + public function testSendEmailOnOrderSave() + { + $this->prepareRequest(['send_confirmation' => true]); + $this->dispatch('backend/sales/order_create/save'); + $this->assertSessionMessages( + $this->equalTo([(string)__('You created the order.')]), + MessageInterface::TYPE_SUCCESS + ); + + $this->assertRedirect($this->stringContains('sales/order/view/')); + + $orderId = $this->getOrderId(); + if ($orderId === false) { + $this->fail('Order is not created.'); + } + $order = $this->getOrder($orderId); + + $message = $this->transportBuilder->getSentMessage(); + $subject = __('Your %1 order confirmation', $order->getStore()->getFrontendName())->render(); + $assert = $this->logicalAnd( + new StringContains($order->getBillingAddress()->getName()), + new StringContains( + 'Thank you for your order from ' . $order->getStore()->getFrontendName() + ), + new StringContains( + "Your Order <span class=\"no-link\">#{$order->getIncrementId()}</span>" + ) + ); + + $this->assertEquals($message->getSubject(), $subject); + $this->assertThat($message->getRawMessage(), $assert); + } + /** * Gets quote by reserved order id. * @@ -82,4 +161,78 @@ private function getQuote($reservedOrderId) $items = $quoteRepository->getList($searchCriteria)->getItems(); return array_pop($items); } + + /** + * @inheritdoc + * @magentoDbIsolation enabled + * @magentoDataFixture Magento/Sales/_files/guest_quote_with_addresses.php + */ + public function testAclHasAccess() + { + $this->prepareRequest(); + + parent::testAclHasAccess(); + } + + /** + * @inheritdoc + * @magentoDbIsolation enabled + * @magentoDataFixture Magento/Sales/_files/guest_quote_with_addresses.php + */ + public function testAclNoAccess() + { + $this->prepareRequest(); + + parent::testAclNoAccess(); + } + + /** + * @param int $orderId + * @return OrderInterface + */ + private function getOrder(int $orderId): OrderInterface + { + return $this->_objectManager->get(OrderRepository::class)->get($orderId); + } + + /** + * @param array $params + * @return void + */ + private function prepareRequest(array $params = []) + { + $quote = $this->getQuote('guest_quote'); + $session = $this->_objectManager->get(Quote::class); + $session->setQuoteId($quote->getId()); + $session->setCustomerId(0); + + $email = 'john.doe001@test.com'; + $data = [ + 'account' => [ + 'email' => $email, + ], + ]; + + $data = array_replace_recursive($data, $params); + + $this->getRequest() + ->setMethod('POST') + ->setParams(['form_key' => $this->formKey->getFormKey()]) + ->setPostValue(['order' => $data]); + } + + /** + * @return string|bool + */ + protected function getOrderId() + { + $currentUrl = $this->getResponse()->getHeader('Location'); + $orderId = false; + + if (preg_match('/order_id\/(?<order_id>\d+)/', $currentUrl, $matches)) { + $orderId = $matches['order_id'] ?? ''; + } + + return $orderId; + } } diff --git a/dev/tests/integration/testsuite/Magento/Sales/Controller/Adminhtml/Order/Creditmemo/AbstractCreditmemoControllerTest.php b/dev/tests/integration/testsuite/Magento/Sales/Controller/Adminhtml/Order/Creditmemo/AbstractCreditmemoControllerTest.php new file mode 100644 index 0000000000000..2a7731715021b --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Sales/Controller/Adminhtml/Order/Creditmemo/AbstractCreditmemoControllerTest.php @@ -0,0 +1,92 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Sales\Controller\Adminhtml\Order\Creditmemo; + +use Magento\Framework\Api\SearchCriteria; +use Magento\Framework\Api\SearchCriteriaBuilder; +use Magento\Framework\Data\Form\FormKey; +use Magento\Sales\Api\Data\CreditmemoInterface; +use Magento\Sales\Api\Data\OrderInterface; +use Magento\Sales\Model\OrderRepository; +use Magento\TestFramework\Mail\Template\TransportBuilderMock; +use Magento\TestFramework\TestCase\AbstractBackendController; + +/** + * Abstract backend creditmemo test. + */ +class AbstractCreditmemoControllerTest extends AbstractBackendController +{ + /** + * @var TransportBuilderMock + */ + protected $transportBuilder; + + /** + * @var OrderRepository + */ + protected $orderRepository; + + /** + * @var FormKey + */ + protected $formKey; + + /** + * @var string + */ + protected $resource = 'Magento_Sales::sales_creditmemo'; + + /** + * @inheritdoc + */ + protected function setUp() + { + parent::setUp(); + $this->transportBuilder = $this->_objectManager->get(TransportBuilderMock::class); + $this->orderRepository = $this->_objectManager->get(OrderRepository::class); + $this->formKey = $this->_objectManager->get(FormKey::class); + } + + /** + * @param string $incrementalId + * @return OrderInterface|null + */ + protected function getOrder(string $incrementalId) + { + /** @var SearchCriteria $searchCriteria */ + $searchCriteria = $this->_objectManager->create(SearchCriteriaBuilder::class) + ->addFilter(OrderInterface::INCREMENT_ID, $incrementalId) + ->create(); + + $orders = $this->orderRepository->getList($searchCriteria)->getItems(); + /** @var OrderInterface|null $order */ + $order = reset($orders); + + return $order; + } + + /** + * @param OrderInterface $order + * @return CreditmemoInterface + */ + protected function getCreditMemo(OrderInterface $order): CreditmemoInterface + { + /** @var \Magento\Sales\Model\ResourceModel\Order\Creditmemo\Collection $creditMemoCollection */ + $creditMemoCollection = $this->_objectManager->create( + \Magento\Sales\Model\ResourceModel\Order\Creditmemo\CollectionFactory::class + )->create(); + + /** @var CreditmemoInterface $creditMemo */ + $creditMemo = $creditMemoCollection + ->setOrderFilter($order) + ->setPageSize(1) + ->getFirstItem(); + + return $creditMemo; + } +} diff --git a/dev/tests/integration/testsuite/Magento/Sales/Controller/Adminhtml/Order/Creditmemo/AddCommentTest.php b/dev/tests/integration/testsuite/Magento/Sales/Controller/Adminhtml/Order/Creditmemo/AddCommentTest.php new file mode 100644 index 0000000000000..ac11f777daf9c --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Sales/Controller/Adminhtml/Order/Creditmemo/AddCommentTest.php @@ -0,0 +1,102 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Sales\Controller\Adminhtml\Order\Creditmemo; + +use PHPUnit\Framework\Constraint\RegularExpression; +use PHPUnit\Framework\Constraint\StringContains; + +/** + * Class verifies creditmemo add comment functionality. + * + * @magentoDbIsolation enabled + * @magentoAppArea adminhtml + * @magentoDataFixture Magento/Sales/_files/creditmemo_for_get.php + */ +class AddCommentTest extends AbstractCreditmemoControllerTest +{ + /** + * @var string + */ + protected $uri = 'backend/sales/order_creditmemo/addComment'; + + /** + * @return void + */ + public function testSendEmailOnAddCreditmemoComment() + { + $comment = 'Test Credit Memo Comment'; + $order = $this->prepareRequest( + [ + 'comment' => ['comment' => $comment, 'is_customer_notified' => true], + ] + ); + $this->dispatch('backend/sales/order_creditmemo/addComment'); + $html = $this->getResponse()->getBody(); + $this->assertContains($comment, $html); + + $message = $this->transportBuilder->getSentMessage(); + $subject =__('Update to your %1 credit memo', $order->getStore()->getFrontendName())->render(); + $messageConstraint = $this->logicalAnd( + new StringContains($order->getBillingAddress()->getName()), + new RegularExpression( + sprintf( + "/Your order #%s has been updated with a status of.*%s/", + $order->getIncrementId(), + $order->getFrontendStatusLabel() + ) + ), + new StringContains($comment) + ); + + $this->assertEquals($message->getSubject(), $subject); + $this->assertThat($message->getRawMessage(), $messageConstraint); + } + + /** + * @inheritdoc + */ + public function testAclHasAccess() + { + $this->prepareRequest(['comment' => ['comment' => 'Comment']]); + + parent::testAclHasAccess(); + } + + /** + * @inheritdoc + */ + public function testAclNoAccess() + { + $this->prepareRequest(['comment' => ['comment' => 'Comment']]); + + parent::testAclNoAccess(); + } + + /** + * @param array $params + * @return \Magento\Sales\Api\Data\OrderInterface|null + */ + private function prepareRequest(array $params = []) + { + $order = $this->getOrder('100000001'); + $creditmemo = $this->getCreditMemo($order); + + $this->getRequest()->setMethod('POST'); + $this->getRequest()->setParams( + [ + 'id' => $creditmemo->getEntityId(), + 'form_key' => $this->formKey->getFormKey(), + ] + ); + + $data = $params ?? []; + $this->getRequest()->setPostValue($data); + + return $order; + } +} diff --git a/dev/tests/integration/testsuite/Magento/Sales/Controller/Adminhtml/Order/Creditmemo/SaveTest.php b/dev/tests/integration/testsuite/Magento/Sales/Controller/Adminhtml/Order/Creditmemo/SaveTest.php new file mode 100644 index 0000000000000..4df7710bb4388 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Sales/Controller/Adminhtml/Order/Creditmemo/SaveTest.php @@ -0,0 +1,99 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Sales\Controller\Adminhtml\Order\Creditmemo; + +use PHPUnit\Framework\Constraint\StringContains; + +/** + * Class tests creditmemo creation in backend. + * + * @magentoDbIsolation enabled + * @magentoAppArea adminhtml + * @magentoDataFixture Magento/Sales/_files/invoice.php + */ +class SaveTest extends AbstractCreditmemoControllerTest +{ + /** + * @var string + */ + protected $uri = 'backend/sales/order_creditmemo/save'; + + /** + * @return void + */ + public function testSendEmailOnCreditmemoSave() + { + $order = $this->prepareRequest(['creditmemo' => ['send_email' => true]]); + $this->dispatch('backend/sales/order_creditmemo/save'); + + $this->assertSessionMessages( + $this->equalTo([(string)__('You created the credit memo.')]), + \Magento\Framework\Message\MessageInterface::TYPE_SUCCESS + ); + $this->assertRedirect($this->stringContains('sales/order/view/order_id/' . $order->getEntityId())); + + $creditMemo = $this->getCreditMemo($order); + $message = $this->transportBuilder->getSentMessage(); + $subject = __('Credit memo for your %1 order', $order->getStore()->getFrontendName())->render(); + $messageConstraint = $this->logicalAnd( + new StringContains($order->getBillingAddress()->getName()), + new StringContains( + 'Thank you for your order from ' . $creditMemo->getStore()->getFrontendName() + ), + new StringContains( + "Your Credit Memo #{$creditMemo->getIncrementId()} for Order #{$order->getIncrementId()}" + ) + ); + + $this->assertEquals($message->getSubject(), $subject); + $this->assertThat($message->getRawMessage(), $messageConstraint); + } + + /** + * @inheritdoc + */ + public function testAclHasAccess() + { + $this->prepareRequest(); + + parent::testAclHasAccess(); + } + + /** + * @inheritdoc + */ + public function testAclNoAccess() + { + $this->prepareRequest(); + + parent::testAclNoAccess(); + } + + /** + * @param array $params + * @return \Magento\Sales\Api\Data\OrderInterface|null + */ + private function prepareRequest(array $params = []) + { + $order = $this->getOrder('100000001'); + $this->getRequest()->setMethod('POST'); + $this->getRequest()->setParams( + [ + 'order_id' => $order->getEntityId(), + 'form_key' => $this->formKey->getFormKey(), + ] + ); + + $data = ['creditmemo' => ['do_offline' => true]]; + $data = array_replace_recursive($data, $params); + + $this->getRequest()->setPostValue($data); + + return $order; + } +} diff --git a/dev/tests/integration/testsuite/Magento/Sales/Controller/Adminhtml/Order/EmailTest.php b/dev/tests/integration/testsuite/Magento/Sales/Controller/Adminhtml/Order/EmailTest.php new file mode 100644 index 0000000000000..337ad206ade91 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Sales/Controller/Adminhtml/Order/EmailTest.php @@ -0,0 +1,136 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Sales\Controller\Adminhtml\Order; + +use Magento\Framework\Api\SearchCriteria; +use Magento\Framework\Api\SearchCriteriaBuilder; +use Magento\Sales\Api\Data\OrderInterface; +use Magento\Sales\Model\OrderRepository; +use Magento\TestFramework\Mail\Template\TransportBuilderMock; +use PHPUnit\Framework\Constraint\StringContains; + +/** + * Class verifies order send email functionality. + * + * @magentoDbIsolation enabled + * @magentoAppArea adminhtml + * @magentoDataFixture Magento/Sales/_files/order.php + */ +class EmailTest extends \Magento\TestFramework\TestCase\AbstractBackendController +{ + /** + * @var OrderRepository + */ + private $orderRepository; + + /** + * @var TransportBuilderMock + */ + private $transportBuilder; + + /** + * @var string + */ + protected $resource = 'Magento_Sales::email'; + + /** + * @var string + */ + protected $uri = 'backend/sales/order/email'; + + /** + * @inheritdoc + */ + protected function setUp() + { + parent::setUp(); + $this->orderRepository = $this->_objectManager->get(OrderRepository::class); + $this->transportBuilder = $this->_objectManager->get(TransportBuilderMock::class); + } + + /** + * @return void + */ + public function testSendOrderEmail() + { + $order = $this->prepareRequest(); + $this->dispatch('backend/sales/order/email'); + + $this->assertSessionMessages( + $this->equalTo([(string)__('You sent the order email.')]), + \Magento\Framework\Message\MessageInterface::TYPE_SUCCESS + ); + + $redirectUrl = 'sales/order/view/order_id/' . $order->getEntityId(); + $this->assertRedirect($this->stringContains($redirectUrl)); + + $message = $this->transportBuilder->getSentMessage(); + $subject = __('Your %1 order confirmation', $order->getStore()->getFrontendName())->render(); + $assert = $this->logicalAnd( + new StringContains($order->getBillingAddress()->getName()), + new StringContains( + 'Thank you for your order from ' . $order->getStore()->getFrontendName() + ), + new StringContains( + "Your Order <span class=\"no-link\">#{$order->getIncrementId()}</span>" + ) + ); + + $this->assertEquals($message->getSubject(), $subject); + $this->assertThat($message->getRawMessage(), $assert); + } + + /** + * @inheritdoc + */ + public function testAclHasAccess() + { + $this->prepareRequest(); + + parent::testAclHasAccess(); + } + + /** + * @inheritdoc + */ + public function testAclNoAccess() + { + $this->prepareRequest(); + + parent::testAclNoAccess(); + } + + /** + * @param string $incrementalId + * @return OrderInterface|null + */ + private function getOrder(string $incrementalId) + { + /** @var SearchCriteria $searchCriteria */ + $searchCriteria = $this->_objectManager->create(SearchCriteriaBuilder::class) + ->addFilter(OrderInterface::INCREMENT_ID, $incrementalId) + ->create(); + + $orders = $this->orderRepository->getList($searchCriteria)->getItems(); + /** @var OrderInterface|null $order */ + $order = reset($orders); + + return $order; + } + + /** + * @return OrderInterface|null + */ + private function prepareRequest() + { + $order = $this->getOrder('100000001'); + $this->getRequest()->setParams(['order_id' => $order->getEntityId()]); + + return $order; + } +} diff --git a/dev/tests/integration/testsuite/Magento/Sales/Controller/Adminhtml/Order/Invoice/AbstractInvoiceControllerTest.php b/dev/tests/integration/testsuite/Magento/Sales/Controller/Adminhtml/Order/Invoice/AbstractInvoiceControllerTest.php new file mode 100644 index 0000000000000..3ba54418b6c26 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Sales/Controller/Adminhtml/Order/Invoice/AbstractInvoiceControllerTest.php @@ -0,0 +1,92 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Sales\Controller\Adminhtml\Order\Invoice; + +use Magento\Framework\Api\SearchCriteria; +use Magento\Framework\Api\SearchCriteriaBuilder; +use Magento\Framework\Data\Form\FormKey; +use Magento\Sales\Api\Data\InvoiceInterface; +use Magento\Sales\Api\Data\OrderInterface; +use Magento\Sales\Model\OrderRepository; +use Magento\TestFramework\Mail\Template\TransportBuilderMock; +use Magento\TestFramework\TestCase\AbstractBackendController; + +/** + * Abstract backend invoice test. + */ +class AbstractInvoiceControllerTest extends AbstractBackendController +{ + /** + * @var TransportBuilderMock + */ + protected $transportBuilder; + + /** + * @var OrderRepository + */ + protected $orderRepository; + + /** + * @var FormKey + */ + protected $formKey; + + /** + * @var string + */ + protected $resource = 'Magento_Sales::sales_invoice'; + + /** + * @inheritdoc + */ + protected function setUp() + { + parent::setUp(); + $this->transportBuilder = $this->_objectManager->get(TransportBuilderMock::class); + $this->orderRepository = $this->_objectManager->get(OrderRepository::class); + $this->formKey = $this->_objectManager->get(FormKey::class); + } + + /** + * @param string $incrementalId + * @return OrderInterface|null + */ + protected function getOrder(string $incrementalId) + { + /** @var SearchCriteria $searchCriteria */ + $searchCriteria = $this->_objectManager->create(SearchCriteriaBuilder::class) + ->addFilter(OrderInterface::INCREMENT_ID, $incrementalId) + ->create(); + + $orders = $this->orderRepository->getList($searchCriteria)->getItems(); + /** @var OrderInterface $order */ + $order = reset($orders); + + return $order; + } + + /** + * @param OrderInterface $order + * @return InvoiceInterface + */ + protected function getInvoiceByOrder(OrderInterface $order): InvoiceInterface + { + /** @var \Magento\Sales\Model\ResourceModel\Order\Invoice\Collection $invoiceCollection */ + $invoiceCollection = $this->_objectManager->create( + \Magento\Sales\Model\ResourceModel\Order\Invoice\CollectionFactory::class + )->create(); + + /** @var InvoiceInterface $invoice */ + $invoice = $invoiceCollection + ->setOrderFilter($order) + ->setPageSize(1) + ->getFirstItem(); + + return $invoice; + } +} diff --git a/dev/tests/integration/testsuite/Magento/Sales/Controller/Adminhtml/Order/Invoice/AddCommentTest.php b/dev/tests/integration/testsuite/Magento/Sales/Controller/Adminhtml/Order/Invoice/AddCommentTest.php new file mode 100644 index 0000000000000..8643dfc66f1b9 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Sales/Controller/Adminhtml/Order/Invoice/AddCommentTest.php @@ -0,0 +1,103 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Sales\Controller\Adminhtml\Order\Invoice; + +use PHPUnit\Framework\Constraint\RegularExpression; +use PHPUnit\Framework\Constraint\StringContains; + +/** + * Class verifies invoice add comment functionality. + * + * @magentoDbIsolation enabled + * @magentoAppArea adminhtml + * @magentoDataFixture Magento/Sales/_files/invoice.php + */ +class AddCommentTest extends AbstractInvoiceControllerTest +{ + /** + * @var string + */ + protected $uri = 'backend/sales/order_invoice/addComment'; + + /** + * @return void + */ + public function testSendEmailOnAddInvoiceComment() + { + $comment = 'Test Invoice Comment'; + $order = $this->prepareRequest( + [ + 'comment' => ['comment' => $comment, 'is_customer_notified' => true], + ] + ); + $this->dispatch('backend/sales/order_invoice/addComment'); + + $html = $this->getResponse()->getBody(); + $this->assertContains($comment, $html); + + $message = $this->transportBuilder->getSentMessage(); + $subject = __('Update to your %1 invoice', $order->getStore()->getFrontendName())->render(); + $messageConstraint = $this->logicalAnd( + new StringContains($order->getBillingAddress()->getName()), + new RegularExpression( + sprintf( + "/Your order #%s has been updated with a status of.*%s/", + $order->getIncrementId(), + $order->getFrontendStatusLabel() + ) + ), + new StringContains($comment) + ); + + $this->assertEquals($message->getSubject(), $subject); + $this->assertThat($message->getRawMessage(), $messageConstraint); + } + + /** + * @inheritdoc + */ + public function testAclHasAccess() + { + $this->prepareRequest(['comment' => ['comment' => 'Comment']]); + + parent::testAclHasAccess(); + } + + /** + * @inheritdoc + */ + public function testAclNoAccess() + { + $this->prepareRequest(['comment' => ['comment' => 'Comment']]); + + parent::testAclNoAccess(); + } + + /** + * @param array $params + * @return \Magento\Sales\Api\Data\OrderInterface|null + */ + private function prepareRequest(array $params = []) + { + $order = $this->getOrder('100000001'); + $invoice = $this->getInvoiceByOrder($order); + + $this->getRequest()->setMethod('POST'); + $this->getRequest()->setParams( + [ + 'id' => $invoice->getEntityId(), + 'form_key' => $this->formKey->getFormKey(), + ] + ); + + $data = $params ?? []; + $this->getRequest()->setPostValue($data); + + return $order; + } +} diff --git a/dev/tests/integration/testsuite/Magento/Sales/Controller/Adminhtml/Order/Invoice/EmailTest.php b/dev/tests/integration/testsuite/Magento/Sales/Controller/Adminhtml/Order/Invoice/EmailTest.php new file mode 100644 index 0000000000000..39b7fc8ef0267 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Sales/Controller/Adminhtml/Order/Invoice/EmailTest.php @@ -0,0 +1,88 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Sales\Controller\Adminhtml\Order\Invoice; + +use PHPUnit\Framework\Constraint\StringContains; + +/** + * Class verifies invoice send email functionality. + * + * @magentoDbIsolation enabled + * @magentoAppArea adminhtml + * @magentoDataFixture Magento/Sales/_files/invoice.php + */ +class EmailTest extends AbstractInvoiceControllerTest +{ + /** + * @var string + */ + protected $uri = 'backend/sales/order_invoice/email'; + + /** + * @return void + */ + public function testSendInvoiceEmail() + { + $order = $this->getOrder('100000001'); + $invoice = $this->getInvoiceByOrder($order); + + $this->getRequest()->setParams(['invoice_id' => $invoice->getEntityId()]); + $this->dispatch('backend/sales/order_invoice/email'); + + $this->assertSessionMessages( + $this->equalTo([(string)__('You sent the message.')]), + \Magento\Framework\Message\MessageInterface::TYPE_SUCCESS + ); + + $redirectUrl = sprintf( + 'sales/invoice/view/order_id/%s/invoice_id/%s', + $order->getEntityId(), + $invoice->getEntityId() + ); + $this->assertRedirect($this->stringContains($redirectUrl)); + + $message = $this->transportBuilder->getSentMessage(); + $subject = __('Invoice for your %1 order', $order->getStore()->getFrontendName())->render(); + $messageConstraint = $this->logicalAnd( + new StringContains($invoice->getBillingAddress()->getName()), + new StringContains( + 'Thank you for your order from ' . $invoice->getStore()->getFrontendName() + ), + new StringContains( + "Your Invoice #{$invoice->getIncrementId()} for Order #{$order->getIncrementId()}" + ) + ); + + $this->assertEquals($message->getSubject(), $subject); + $this->assertThat($message->getRawMessage(), $messageConstraint); + } + + /** + * @inheritdoc + */ + public function testAclHasAccess() + { + $order = $this->getOrder('100000001'); + $invoice = $this->getInvoiceByOrder($order); + $this->uri .= '/invoice_id/' . $invoice->getEntityId(); + + parent::testAclHasAccess(); + } + + /** + * @inheritdoc + */ + public function testAclNoAccess() + { + $order = $this->getOrder('100000001'); + $invoice = $this->getInvoiceByOrder($order); + $this->uri .= '/invoice_id/' . $invoice->getEntityId(); + + parent::testAclNoAccess(); + } +} diff --git a/dev/tests/integration/testsuite/Magento/Sales/Controller/Adminhtml/Order/Invoice/SaveTest.php b/dev/tests/integration/testsuite/Magento/Sales/Controller/Adminhtml/Order/Invoice/SaveTest.php new file mode 100644 index 0000000000000..d451bdcb287cf --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Sales/Controller/Adminhtml/Order/Invoice/SaveTest.php @@ -0,0 +1,97 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Sales\Controller\Adminhtml\Order\Invoice; + +use PHPUnit\Framework\Constraint\StringContains; + +/** + * Class tests invoice creation in backend. + * + * @magentoDbIsolation enabled + * @magentoAppArea adminhtml + * @magentoDataFixture Magento/Sales/_files/order.php + */ +class SaveTest extends AbstractInvoiceControllerTest +{ + /** + * @var string + */ + protected $uri = 'backend/sales/order_invoice/save'; + + /** + * @return void + */ + public function testSendEmailOnInvoiceSave() + { + $order = $this->prepareRequest(['invoice' => ['send_email' => true]]); + $this->dispatch('backend/sales/order_invoice/save'); + + $this->assertSessionMessages( + $this->equalTo([(string)__('The invoice has been created.')]), + \Magento\Framework\Message\MessageInterface::TYPE_SUCCESS + ); + $this->assertRedirect($this->stringContains('sales/order/view/order_id/' . $order->getEntityId())); + + $invoice = $this->getInvoiceByOrder($order); + $message = $this->transportBuilder->getSentMessage(); + $subject = __('Invoice for your %1 order', $order->getStore()->getFrontendName())->render(); + $messageConstraint = $this->logicalAnd( + new StringContains($invoice->getBillingAddress()->getName()), + new StringContains( + 'Thank you for your order from ' . $invoice->getStore()->getFrontendName() + ), + new StringContains( + "Your Invoice #{$invoice->getIncrementId()} for Order #{$order->getIncrementId()}" + ) + ); + + $this->assertEquals($message->getSubject(), $subject); + $this->assertThat($message->getRawMessage(), $messageConstraint); + } + + /** + * @inheritdoc + */ + public function testAclHasAccess() + { + $this->prepareRequest(); + + parent::testAclHasAccess(); + } + + /** + * @inheritdoc + */ + public function testAclNoAccess() + { + $this->prepareRequest(); + + parent::testAclNoAccess(); + } + + /** + * @param array $params + * @return \Magento\Sales\Api\Data\OrderInterface|null + */ + private function prepareRequest(array $params = []) + { + $order = $this->getOrder('100000001'); + $this->getRequest()->setMethod('POST'); + $this->getRequest()->setParams( + [ + 'order_id' => $order->getEntityId(), + 'form_key' => $this->formKey->getFormKey(), + ] + ); + + $data = $params ?? []; + $this->getRequest()->setPostValue($data); + + return $order; + } +} diff --git a/dev/tests/integration/testsuite/Magento/Sales/CustomerData/LastOrderedItemsTest.php b/dev/tests/integration/testsuite/Magento/Sales/CustomerData/LastOrderedItemsTest.php new file mode 100644 index 0000000000000..adeff2c89a241 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Sales/CustomerData/LastOrderedItemsTest.php @@ -0,0 +1,55 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +namespace Magento\Sales\CustomerData; + +use Magento\Framework\ObjectManagerInterface; +use Magento\TestFramework\Helper\Bootstrap; +use PHPUnit\Framework\TestCase; +use Magento\Customer\Model\Session; + +/** + * Test for LastOrderedItems. + * + * @magentoAppIsolation enabled + */ +class LastOrderedItemsTest extends TestCase +{ + /** + * @var ObjectManagerInterface + */ + private $objectManager; + + /** + * @inheritdoc + */ + public function setUp() + { + $this->objectManager = Bootstrap::getObjectManager(); + } + + /** + * Test to check count in items collection. + * + * @magentoDataFixture Magento/Sales/_files/order_with_customer_and_multiple_order_items.php + */ + public function testDefaultFormatterIsAppliedWhenBasicIntegration() + { + /** @var Session $customerSession */ + $customerSession = $this->objectManager->get(Session::class); + $customerSession->loginById(1); + + /** @var LastOrderedItems $customerDataSectionSource */ + $customerDataSectionSource = $this->objectManager->get(LastOrderedItems::class); + $data = $customerDataSectionSource->getSectionData(); + + $this->assertEquals( + LastOrderedItems::SIDEBAR_ORDER_LIMIT, + count($data['items']), + 'Section items count should not be greater then ' . LastOrderedItems::SIDEBAR_ORDER_LIMIT + ); + } +} diff --git a/dev/tests/integration/testsuite/Magento/Sales/Model/Order/CreateTest.php b/dev/tests/integration/testsuite/Magento/Sales/Model/Order/CreateTest.php new file mode 100644 index 0000000000000..4ff4ad384d3e4 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Sales/Model/Order/CreateTest.php @@ -0,0 +1,100 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Sales\Model\Order; + +use Magento\Checkout\Model\Session as CheckoutSession; +use Magento\Framework\Data\Form\FormKey; +use Magento\Framework\ObjectManagerInterface; +use Magento\Quote\Api\GuestCartManagementInterface; +use Magento\Quote\Model\Quote; +use Magento\Quote\Model\QuoteIdMask; +use Magento\Quote\Model\QuoteIdMaskFactory; +use Magento\Sales\Model\OrderRepository; +use Magento\TestFramework\Helper\Bootstrap; +use Magento\TestFramework\Mail\Template\TransportBuilderMock; +use PHPUnit\Framework\Constraint\StringContains; + +/** + * Class verifies order creation. + * + * @magentoDbIsolation enabled + * @magentoAppArea frontend + */ +class CreateTest extends \PHPUnit\Framework\TestCase +{ + /** + * @var ObjectManagerInterface + */ + private $objectManager; + + /** + * @var TransportBuilderMock + */ + private $transportBuilder; + + /** + * @var QuoteIdMaskFactory + */ + private $quoteIdMaskFactory; + + /** + * @var FormKey + */ + private $formKey; + + /** + * @inheritdoc + */ + protected function setUp() + { + parent::setUp(); + $this->objectManager = Bootstrap::getObjectManager(); + $this->transportBuilder = $this->objectManager->get(TransportBuilderMock::class); + $this->quoteIdMaskFactory = $this->objectManager->get(QuoteIdMaskFactory::class); + $this->formKey = $this->objectManager->get(FormKey::class); + } + + /** + * @magentoDataFixture Magento/Sales/_files/guest_quote_with_addresses.php + * @return void + */ + public function testSendEmailOnOrderPlace() + { + /** @var Quote $quote */ + $quote = $this->objectManager->create(Quote::class); + $quote->load('guest_quote', 'reserved_order_id'); + + $checkoutSession = $this->objectManager->get(CheckoutSession::class); + $checkoutSession->setQuoteId($quote->getId()); + + /** @var QuoteIdMask $quoteIdMask */ + $quoteIdMask = $this->quoteIdMaskFactory->create(); + $quoteIdMask->load($quote->getId(), 'quote_id'); + $cartId = $quoteIdMask->getMaskedId(); + + /** @var GuestCartManagementInterface $cartManagement */ + $cartManagement = $this->objectManager->get(GuestCartManagementInterface::class); + $orderId = $cartManagement->placeOrder($cartId); + $order = $this->objectManager->get(OrderRepository::class)->get($orderId); + + $message = $this->transportBuilder->getSentMessage(); + $subject = __('Your %1 order confirmation', $order->getStore()->getFrontendName())->render(); + $assert = $this->logicalAnd( + new StringContains($order->getBillingAddress()->getName()), + new StringContains( + 'Thank you for your order from ' . $order->getStore()->getFrontendName() + ), + new StringContains( + "Your Order <span class=\"no-link\">#{$order->getIncrementId()}</span>" + ) + ); + + $this->assertEquals($message->getSubject(), $subject); + $this->assertThat($message->getRawMessage(), $assert); + } +} diff --git a/dev/tests/integration/testsuite/Magento/Sales/_files/guest_quote_with_addresses.php b/dev/tests/integration/testsuite/Magento/Sales/_files/guest_quote_with_addresses.php new file mode 100644 index 0000000000000..601c500c18429 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Sales/_files/guest_quote_with_addresses.php @@ -0,0 +1,68 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +require __DIR__ . '/address_list.php'; + +\Magento\TestFramework\Helper\Bootstrap::getInstance()->loadArea('frontend'); + +$objectManager = \Magento\TestFramework\Helper\Bootstrap::getObjectManager(); +/** @var \Magento\Catalog\Model\Product $product */ +$product = $objectManager->create(\Magento\Catalog\Model\Product::class); +$product->setTypeId('simple') + ->setAttributeSetId($product->getDefaultAttributeSetId()) + ->setName('Simple Product') + ->setSku('simple-product-guest-quote') + ->setPrice(10) + ->setTaxClassId(0) + ->setMetaTitle('meta title') + ->setMetaKeyword('meta keyword') + ->setMetaDescription('meta description') + ->setVisibility(\Magento\Catalog\Model\Product\Visibility::VISIBILITY_BOTH) + ->setStatus(\Magento\Catalog\Model\Product\Attribute\Source\Status::STATUS_ENABLED) + ->setStockData( + [ + 'qty' => 100, + 'is_in_stock' => 1, + ] + )->save(); + +$productRepository = $objectManager->create(\Magento\Catalog\Api\ProductRepositoryInterface::class); +$product = $productRepository->get('simple-product-guest-quote'); + +$addressData = reset($addresses); + +$billingAddress = $objectManager->create( + \Magento\Quote\Model\Quote\Address::class, + ['data' => $addressData] +); +$billingAddress->setAddressType('billing'); + +$shippingAddress = clone $billingAddress; +$shippingAddress->setId(null)->setAddressType('shipping'); + +$store = $objectManager->get(\Magento\Store\Model\StoreManagerInterface::class)->getStore(); + +/** @var \Magento\Quote\Model\Quote $quote */ +$quote = $objectManager->create(\Magento\Quote\Model\Quote::class); +$quote->setCustomerIsGuest(true) + ->setStoreId($store->getId()) + ->setReservedOrderId('guest_quote') + ->setBillingAddress($billingAddress) + ->setShippingAddress($shippingAddress) + ->addProduct($product); +$quote->getPayment()->setMethod('checkmo'); +$quote->getShippingAddress()->setShippingMethod('flatrate_flatrate')->setCollectShippingRates(1); +$quote->collectTotals(); + +$quoteRepository = $objectManager->create(\Magento\Quote\Api\CartRepositoryInterface::class); +$quoteRepository->save($quote); + +/** @var \Magento\Quote\Model\QuoteIdMask $quoteIdMask */ +$quoteIdMask = $objectManager->create(\Magento\Quote\Model\QuoteIdMaskFactory::class)->create(); +$quoteIdMask->setQuoteId($quote->getId()); +$quoteIdMask->setDataChanges(true); +$quoteIdMask->save(); diff --git a/dev/tests/integration/testsuite/Magento/Sales/_files/guest_quote_with_addresses_rollback.php b/dev/tests/integration/testsuite/Magento/Sales/_files/guest_quote_with_addresses_rollback.php new file mode 100644 index 0000000000000..02c42153b72c3 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Sales/_files/guest_quote_with_addresses_rollback.php @@ -0,0 +1,32 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +/** @var \Magento\Framework\Registry $registry */ +$objectManager = \Magento\TestFramework\Helper\Bootstrap::getObjectManager(); +$registry = $objectManager->get(\Magento\Framework\Registry::class); +$registry->unregister('isSecureArea'); +$registry->register('isSecureArea', true); + +/** @var $quote \Magento\Quote\Model\Quote */ +$quote = $objectManager->create(\Magento\Quote\Model\Quote::class); +$quote->load('guest_quote', 'reserved_order_id'); +if ($quote->getId()) { + $quote->delete(); +} + +/** @var \Magento\Catalog\Api\ProductRepositoryInterface $productRepository */ +$productRepository = $objectManager->create(\Magento\Catalog\Api\ProductRepositoryInterface::class); + +try { + $product = $productRepository->get('simple-product-guest-quote', false, null, true); + $productRepository->delete($product); +} catch (\Magento\Framework\Exception\NoSuchEntityException $exception) { + //Product already removed +} + +$registry->unregister('isSecureArea'); +$registry->register('isSecureArea', false); diff --git a/dev/tests/integration/testsuite/Magento/Sales/_files/order.php b/dev/tests/integration/testsuite/Magento/Sales/_files/order.php index 9f9f2f740c84e..65b143d9d68f7 100644 --- a/dev/tests/integration/testsuite/Magento/Sales/_files/order.php +++ b/dev/tests/integration/testsuite/Magento/Sales/_files/order.php @@ -43,7 +43,8 @@ ->setBasePrice($product->getPrice()) ->setPrice($product->getPrice()) ->setRowTotal($product->getPrice()) - ->setProductType('simple'); + ->setProductType('simple') + ->setName($product->getName()); /** @var Order $order */ $order = $objectManager->create(Order::class); diff --git a/dev/tests/integration/testsuite/Magento/Sales/_files/order_list.php b/dev/tests/integration/testsuite/Magento/Sales/_files/order_list.php index 43e98419798a8..221f5b559c9f2 100644 --- a/dev/tests/integration/testsuite/Magento/Sales/_files/order_list.php +++ b/dev/tests/integration/testsuite/Magento/Sales/_files/order_list.php @@ -5,6 +5,9 @@ */ use Magento\Sales\Model\Order; +use Magento\Sales\Api\OrderRepositoryInterface; +use Magento\Sales\Model\Order\Address as OrderAddress; +use Magento\Sales\Model\Order\Payment; require 'order.php'; /** @var Order $order */ @@ -48,16 +51,43 @@ ], ]; +$orderList = []; +/** @var OrderRepositoryInterface $orderRepository */ +$orderRepository = $objectManager->create(OrderRepositoryInterface::class); /** @var array $orderData */ foreach ($orders as $orderData) { - /** @var $order \Magento\Sales\Model\Order */ - $order = \Magento\TestFramework\Helper\Bootstrap::getObjectManager()->create( - \Magento\Sales\Model\Order::class - ); + /** @var 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 Payment $payment */ + $payment = $objectManager->create(Payment::class); + $payment->setMethod('checkmo') + ->setAdditionalInformation('last_trans_id', '11122') + ->setAdditionalInformation( + 'metadata', + [ + 'type' => 'free', + 'fraudulent' => false, + ] + ); + $order ->setData($orderData) ->addItem($orderItem) + ->setCustomerIsGuest(true) + ->setCustomerEmail('customer@null.com') ->setBillingAddress($billingAddress) - ->setBillingAddress($shippingAddress) - ->save(); + ->setShippingAddress($shippingAddress) + ->setPayment($payment); + + $orderRepository->save($order); + $orderList[] = $order; } diff --git a/dev/tests/integration/testsuite/Magento/Sales/_files/order_list_with_tax.php b/dev/tests/integration/testsuite/Magento/Sales/_files/order_list_with_tax.php new file mode 100644 index 0000000000000..48f6ccfead297 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Sales/_files/order_list_with_tax.php @@ -0,0 +1,55 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +use Magento\Sales\Model\Order\Tax\ItemFactory; +use Magento\Store\Model\StoreManagerInterface; +use Magento\Tax\Model\Sales\Order\TaxFactory; + +require 'default_rollback.php'; +require 'order_list.php'; + +/** @var array $orderList */ +foreach ($orderList as $order) { + $amount = 45; + $taxFactory = $objectManager->create(TaxFactory::class); + + /** @var \Magento\Tax\Model\Sales\Order\Tax $tax */ + $tax = $taxFactory->create(); + $tax->setOrderId($order->getId()) + ->setCode('US-NY-*-Rate 1') + ->setTitle('US-NY-*-Rate 1') + ->setPercent(8.37) + ->setAmount($amount) + ->setBaseAmount($amount) + ->setBaseRealAmount($amount); + $tax->save(); + + $salesOrderFactory = $objectManager->create(ItemFactory::class); + + /** @var \Magento\Sales\Model\Order\Tax\Item $salesOrderItem */ + $salesOrderItem = $salesOrderFactory->create(); + $salesOrderItem->setOrderId($order->getId()) + ->setStoreId($objectManager->get(StoreManagerInterface::class)->getStore()->getId()) + ->setProductOptions([]); + $salesCollection = $objectManager->create(\Magento\Sales\Model\ResourceModel\Order\Item::class); + $salesCollection->save($salesOrderItem); + + /** @var \Magento\Sales\Model\Order\Tax\Item $salesOrderItem */ + $salesOrderTaxItem = $salesOrderFactory->create(); + $salesOrderTaxItem->setTaxId($tax->getId()) + ->setTaxPercent(8.37) + ->setTaxAmount($amount) + ->setBaseAmount($amount) + ->setRealAmount($amount) + ->setRealBaseAmount($amount) + ->setAppliedTaxes([$tax]) + ->setTaxableItemType('shipping') + ->setItemId($salesOrderItem->getId()); + + $taxItemCollection = $objectManager->create(\Magento\Sales\Model\ResourceModel\Order\Tax\Item::class); + $taxItemCollection->save($salesOrderTaxItem); +} diff --git a/dev/tests/integration/testsuite/Magento/Sales/_files/order_list_with_tax_rollback.php b/dev/tests/integration/testsuite/Magento/Sales/_files/order_list_with_tax_rollback.php new file mode 100644 index 0000000000000..dd52deab825cb --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Sales/_files/order_list_with_tax_rollback.php @@ -0,0 +1,8 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +require 'order_rollback.php'; diff --git a/dev/tests/integration/testsuite/Magento/Sales/_files/order_with_customer_and_multiple_order_items.php b/dev/tests/integration/testsuite/Magento/Sales/_files/order_with_customer_and_multiple_order_items.php new file mode 100644 index 0000000000000..586863dd07696 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Sales/_files/order_with_customer_and_multiple_order_items.php @@ -0,0 +1,12 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +require __DIR__ . '/order_with_multiple_items.php'; +require __DIR__ . '/../../../Magento/Customer/_files/customer.php'; + +$customerIdFromFixture = 1; +/** @var $order \Magento\Sales\Model\Order */ +$order->setCustomerId($customerIdFromFixture)->setCustomerIsGuest(false)->save(); diff --git a/dev/tests/integration/testsuite/Magento/Sales/_files/order_with_discount.php b/dev/tests/integration/testsuite/Magento/Sales/_files/order_with_discount.php new file mode 100644 index 0000000000000..29a7aa4d90334 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Sales/_files/order_with_discount.php @@ -0,0 +1,73 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +use Magento\Sales\Api\OrderRepositoryInterface; +use Magento\Sales\Model\Order; +use Magento\Sales\Model\Order\Address as OrderAddress; +use Magento\Sales\Model\Order\Item as OrderItem; +use Magento\Sales\Model\Order\Payment; +use Magento\Store\Model\StoreManagerInterface; + +require __DIR__ . '/../../../Magento/Catalog/_files/product_simple.php'; +/** @var \Magento\Catalog\Model\Product $product */ + +$addressData = include __DIR__ . '/address_data.php'; + +$objectManager = \Magento\TestFramework\Helper\Bootstrap::getObjectManager(); + +$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') + ->setDiscountAmount(2) + ->setBaseRowTotal($product->getPrice()) + ->setBaseDiscountAmount(2) + ->setTaxAmount(1) + ->setBaseTaxAmount(1); + +/** @var Order $order */ +$order = $objectManager->create(Order::class); +$order->setIncrementId('100000001') + ->setState(Order::STATE_PROCESSING) + ->setStatus($order->getConfig()->getStateDefaultStatus(Order::STATE_PROCESSING)) + ->setSubtotal(100) + ->setGrandTotal(100) + ->setBaseSubtotal(100) + ->setBaseGrandTotal(100) + ->setCustomerIsGuest(true) + ->setCustomerEmail('customer@null.com') + ->setBillingAddress($billingAddress) + ->setShippingAddress($shippingAddress) + ->setStoreId($objectManager->get(StoreManagerInterface::class)->getStore()->getId()) + ->addItem($orderItem) + ->setPayment($payment); + +/** @var OrderRepositoryInterface $orderRepository */ +$orderRepository = $objectManager->create(OrderRepositoryInterface::class); +$orderRepository->save($order); diff --git a/dev/tests/integration/testsuite/Magento/Sales/_files/order_with_discount_rollback.php b/dev/tests/integration/testsuite/Magento/Sales/_files/order_with_discount_rollback.php new file mode 100644 index 0000000000000..1fb4b4636ab29 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Sales/_files/order_with_discount_rollback.php @@ -0,0 +1,8 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +require 'default_rollback.php'; diff --git a/dev/tests/integration/testsuite/Magento/Sales/_files/order_with_multiple_items.php b/dev/tests/integration/testsuite/Magento/Sales/_files/order_with_multiple_items.php new file mode 100644 index 0000000000000..8375ae71fd10b --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Sales/_files/order_with_multiple_items.php @@ -0,0 +1,81 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +require 'order.php'; +/** @var \Magento\Catalog\Model\Product $product */ +/** @var \Magento\Sales\Model\Order $order */ +$objectManager = \Magento\TestFramework\Helper\Bootstrap::getObjectManager(); + +require __DIR__ . '/../../../Magento/Catalog/_files/product_simple.php'; +$orderItems[] = [ + 'product_id' => $product->getId(), + 'base_price' => 123, + 'order_id' => $order->getId(), + 'price' => 123, + 'row_total' => 126, + 'product_type' => 'simple' +]; + +require __DIR__ . '/../../../Magento/Catalog/_files/product_simple_duplicated.php'; +$orderItems[] = [ + 'product_id' => $product->getId(), + 'base_price' => 123, + 'order_id' => $order->getId(), + 'price' => 123, + 'row_total' => 126, + 'product_type' => 'simple' +]; + +require __DIR__ . '/../../../Magento/Catalog/_files/product_simple_with_decimal_qty.php'; +$orderItems[] = [ + 'product_id' => $product->getId(), + 'base_price' => 123, + 'order_id' => $order->getId(), + 'price' => 123, + 'row_total' => 126, + 'product_type' => 'simple' +]; + +require __DIR__ . '/../../../Magento/Catalog/_files/product_simple_with_url_key.php'; +$orderItems[] = [ + 'product_id' => $product->getId(), + 'base_price' => 123, + 'order_id' => $order->getId(), + 'price' => 123, + 'row_total' => 126, + 'product_type' => 'simple' +]; + +require __DIR__ . '/../../../Magento/Catalog/_files/product_simple_sku_with_slash.php'; +$orderItems[] = [ + 'product_id' => $product->getId(), + 'base_price' => 123, + 'order_id' => $order->getId(), + 'price' => 123, + 'row_total' => 126, + 'product_type' => 'simple' +]; + +require __DIR__ . '/../../../Magento/Catalog/_files/product_simple_xss.php'; +$orderItems[] = [ + 'product_id' => $product->getId(), + 'base_price' => 123, + 'order_id' => $order->getId(), + 'price' => 123, + 'row_total' => 126, + 'product_type' => 'simple' +]; + +/** @var array $orderItemData */ +foreach ($orderItems as $orderItemData) { + /** @var $orderItem \Magento\Sales\Model\Order\Item */ + $orderItem = \Magento\TestFramework\Helper\Bootstrap::getObjectManager()->create( + \Magento\Sales\Model\Order\Item::class + ); + $orderItem + ->setData($orderItemData) + ->save(); +} diff --git a/dev/tests/integration/testsuite/Magento/Sales/_files/order_with_shipping_and_invoice.php b/dev/tests/integration/testsuite/Magento/Sales/_files/order_with_shipping_and_invoice.php index a889235ea1862..61d8be98bdd22 100644 --- a/dev/tests/integration/testsuite/Magento/Sales/_files/order_with_shipping_and_invoice.php +++ b/dev/tests/integration/testsuite/Magento/Sales/_files/order_with_shipping_and_invoice.php @@ -3,6 +3,9 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ + +use Magento\Sales\Model\Order\ShipmentFactory; + require 'order.php'; $objectManager = \Magento\TestFramework\Helper\Bootstrap::getObjectManager(); @@ -36,4 +39,11 @@ $order->setIsInProcess(true); -$transaction->addObject($invoice)->addObject($order)->save(); +$items = []; +foreach ($order->getItems() as $orderItem) { + $items[$orderItem->getId()] = $orderItem->getQtyOrdered(); +} +$shipment = $objectManager->get(ShipmentFactory::class)->create($order, $items); +$shipment->register(); + +$transaction->addObject($invoice)->addObject($shipment)->addObject($order)->save(); diff --git a/dev/tests/integration/testsuite/Magento/Sales/_files/order_with_tax.php b/dev/tests/integration/testsuite/Magento/Sales/_files/order_with_tax.php new file mode 100644 index 0000000000000..0c7dc522f5759 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Sales/_files/order_with_tax.php @@ -0,0 +1,52 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +use Magento\Sales\Model\Order\Tax\ItemFactory; +use Magento\Store\Model\StoreManagerInterface; +use Magento\Tax\Model\Sales\Order\TaxFactory; + +require 'default_rollback.php'; +require 'order.php'; + +$amount = 45; +$taxFactory = $objectManager->create(TaxFactory::class); + +/** @var \Magento\Tax\Model\Sales\Order\Tax $tax */ +$tax = $taxFactory->create(); +$tax->setOrderId($order->getId()) + ->setCode('US-NY-*-Rate 1') + ->setTitle('US-NY-*-Rate 1') + ->setPercent(8.37) + ->setAmount($amount) + ->setBaseAmount($amount) + ->setBaseRealAmount($amount); +$tax->save(); + +$salesOrderFactory = $objectManager->create(ItemFactory::class); + +/** @var \Magento\Sales\Model\Order\Tax\Item $salesOrderItem */ +$salesOrderItem = $salesOrderFactory->create(); +$salesOrderItem->setOrderId($order->getId()) + ->setStoreId($objectManager->get(StoreManagerInterface::class)->getStore()->getId()) + ->setProductOptions([]); +$salesCollection = $objectManager->create(\Magento\Sales\Model\ResourceModel\Order\Item::class); +$salesCollection->save($salesOrderItem); + +/** @var \Magento\Sales\Model\Order\Tax\Item $salesOrderTaxItem */ +$salesOrderTaxItem = $salesOrderFactory->create(); +$salesOrderTaxItem->setTaxId($tax->getId()) + ->setTaxPercent(8.37) + ->setTaxAmount($amount) + ->setBaseAmount($amount) + ->setRealAmount($amount) + ->setRealBaseAmount($amount) + ->setAppliedTaxes([$tax]) + ->setTaxableItemType('shipping') + ->setItemId($salesOrderItem->getId()); + +$taxItemCollection = $objectManager->create(\Magento\Sales\Model\ResourceModel\Order\Tax\Item::class); +$taxItemCollection->save($salesOrderTaxItem); diff --git a/dev/tests/integration/testsuite/Magento/Sales/_files/order_with_tax_rollback.php b/dev/tests/integration/testsuite/Magento/Sales/_files/order_with_tax_rollback.php new file mode 100644 index 0000000000000..dd52deab825cb --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Sales/_files/order_with_tax_rollback.php @@ -0,0 +1,8 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +require 'order_rollback.php'; diff --git a/dev/tests/integration/testsuite/Magento/Sales/_files/quote_with_custom_price.php b/dev/tests/integration/testsuite/Magento/Sales/_files/quote_with_custom_price.php new file mode 100644 index 0000000000000..3ed44bfd69528 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Sales/_files/quote_with_custom_price.php @@ -0,0 +1,73 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +use Magento\TestFramework\Helper\Bootstrap; + +Bootstrap::getInstance()->loadArea('frontend'); +$product = Bootstrap::getObjectManager()->create(\Magento\Catalog\Model\Product::class); +$product->setTypeId('simple') + ->setAttributeSetId(4) + ->setName('Simple Product') + ->setSku('simple_quote_custom_price') + ->setPrice(10) + ->setTaxClassId(0) + ->setMetaTitle('meta title') + ->setMetaKeyword('meta keyword') + ->setMetaDescription('meta description') + ->setVisibility(\Magento\Catalog\Model\Product\Visibility::VISIBILITY_BOTH) + ->setStatus(\Magento\Catalog\Model\Product\Attribute\Source\Status::STATUS_ENABLED) + ->setStockData( + [ + 'qty' => 100, + 'is_in_stock' => 1, + ] + )->save(); + +$productRepository = Bootstrap::getObjectManager()->create(\Magento\Catalog\Api\ProductRepositoryInterface::class); +$product = $productRepository->get('simple_quote_custom_price'); + +$addressData = include __DIR__ . '/address_data.php'; +$billingAddress = Bootstrap::getObjectManager()->create( + \Magento\Quote\Model\Quote\Address::class, + ['data' => $addressData] +); +$billingAddress->setAddressType('billing'); + +$shippingAddress = clone $billingAddress; +$shippingAddress->setId(null)->setAddressType('shipping'); + +$store = Bootstrap::getObjectManager() + ->get(\Magento\Store\Model\StoreManagerInterface::class) + ->getStore(); + +$requestData = [ + 'qty' => 1, + 'custom_price' => 12, +]; +$request = Bootstrap::getObjectManager()->create(\Magento\Framework\DataObject::class, ['data' => $requestData]); + +/** @var \Magento\Quote\Model\Quote $quote */ +$quote = Bootstrap::getObjectManager()->create(\Magento\Quote\Model\Quote::class); +$quote->setCustomerIsGuest(true) + ->setStoreId($store->getId()) + ->setReservedOrderId('test01') + ->setBillingAddress($billingAddress) + ->setShippingAddress($shippingAddress) + ->addProduct($product, $request); +$quote->getPayment()->setMethod('checkmo'); +$quote->setIsMultiShipping('1'); +$quote->collectTotals(); + +$quoteRepository = Bootstrap::getObjectManager()->create(\Magento\Quote\Api\CartRepositoryInterface::class); +$quoteRepository->save($quote); + +/** @var \Magento\Quote\Model\QuoteIdMask $quoteIdMask */ +$quoteIdMask = Bootstrap::getObjectManager() + ->create(\Magento\Quote\Model\QuoteIdMaskFactory::class) + ->create(); +$quoteIdMask->setQuoteId($quote->getId()); +$quoteIdMask->setDataChanges(true); +$quoteIdMask->save(); diff --git a/dev/tests/integration/testsuite/Magento/Sales/_files/quote_with_custom_price_rollback.php b/dev/tests/integration/testsuite/Magento/Sales/_files/quote_with_custom_price_rollback.php new file mode 100644 index 0000000000000..d17ec28063543 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Sales/_files/quote_with_custom_price_rollback.php @@ -0,0 +1,24 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +include __DIR__ . '/quote_rollback.php'; + +/** @var \Magento\Framework\Registry $registry */ +$registry->unregister('isSecureArea'); +$registry->register('isSecureArea', true); + +/** @var \Magento\Catalog\Api\ProductRepositoryInterface $productRepository */ +$productRepository = \Magento\TestFramework\Helper\Bootstrap::getObjectManager() + ->create(\Magento\Catalog\Api\ProductRepositoryInterface::class); + +try { + $product = $productRepository->get('simple_quote_custom_price', false, null, true); + $productRepository->delete($product); +} catch (\Magento\Framework\Exception\NoSuchEntityException $exception) { + //Product already removed +} + +$registry->unregister('isSecureArea'); +$registry->register('isSecureArea', false); diff --git a/dev/tests/integration/testsuite/Magento/SalesRule/Model/Observer/AssignCouponDataAfterOrderCustomerAssignTest.php b/dev/tests/integration/testsuite/Magento/SalesRule/Model/Observer/AssignCouponDataAfterOrderCustomerAssignTest.php new file mode 100644 index 0000000000000..397650df416e9 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/SalesRule/Model/Observer/AssignCouponDataAfterOrderCustomerAssignTest.php @@ -0,0 +1,291 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +namespace Magento\SalesRule\Model\Observer; + +use Magento\Sales\Model\Order; +use Magento\Customer\Model\GroupManagement; +use Magento\SalesRule\Api\CouponRepositoryInterface; +use Magento\SalesRule\Model\Coupon; +use Magento\SalesRule\Model\Rule; +use Magento\Store\Model\StoreManagerInterface; +use Magento\Customer\Model\Data\Customer; +use Magento\TestFramework\Helper\Bootstrap; + +/** + * Class AssignCouponDataAfterOrderCustomerAssignTest + * + * @magentoAppIsolation enabled + * + * @SuppressWarnings(PHPMD.CouplingBetweenObjects) + */ +class AssignCouponDataAfterOrderCustomerAssignTest extends \PHPUnit\Framework\TestCase +{ + /** + * @var \Magento\Framework\ObjectManagerInterface + */ + private $objectManager; + + /** + * @var \Magento\Quote\Api\GuestCartManagementInterface + */ + private $assignCouponToCustomerObserver; + + /** + * @var Magento\Sales\Model\OrderRepository + */ + private $orderRepository; + + /** + * @var \Magento\Framework\Event\ManagerInterface + */ + protected $eventManager; + + /** + * @var Magento\Customer\Api\CustomerRepositoryInterface + */ + private $customerRepository; + + /** + * @var Order\OrderCustomerDelegate + */ + private $delegateCustomerService; + + /** + * @var Magento\SalesRule\Model\Rule\CustomerFactory + */ + private $ruleCustomerFactory; + + /** + * @var Rule + */ + private $salesRule; + + /** + * @var Coupon + */ + private $coupon; + + /** + * @var Order + */ + private $order; + + /** + * @var Customer + */ + private $customer; + + /** + * @inheritdoc + */ + protected function setUp() + { + $this->objectManager = Bootstrap::getObjectManager(); + $this->eventManager = $this->createMock(\Magento\Framework\Event\ManagerInterface::class); + $this->orderRepository = $this->objectManager->get(\Magento\Sales\Model\OrderRepository::class); + $this->delegateCustomerService = $this->objectManager->get(Order\OrderCustomerDelegate::class); + $this->customerRepository = $this->objectManager->get(\Magento\Customer\Api\CustomerRepositoryInterface::class); + $this->ruleCustomerFactory = $this->objectManager->get(\Magento\SalesRule\Model\Rule\CustomerFactory::class); + $this->assignCouponToCustomerObserver = $this->objectManager->get( + \Magento\SalesRule\Observer\AssignCouponDataAfterOrderCustomerAssignObserver::class + ); + + $this->salesRule = $this->prepareSalesRule(); + $this->coupon = $this->attachSalesruleCoupon($this->salesRule); + $this->order = $this->makeOrderWithCouponAsGuest($this->coupon); + $this->delegateOrderToBeAssigned($this->order); + $this->customer = $this->registerNewCustomer(); + $this->order->setCustomerId($this->customer->getId()); + } + + /** + * @inheritdoc + */ + protected function tearDown() + { + $this->salesRule = null; + $this->customer = null; + $this->coupon = null; + $this->order = null; + } + + /** + * @magentoDataFixture Magento/Sales/_files/order.php + */ + public function testCouponDataHasBeenAssignedTest() + { + $ruleCustomer = $this->getSalesruleCustomerUsage($this->customer, $this->salesRule); + + // Assert, that rule customer model has been created for specific customer + $this->assertEquals( + $ruleCustomer->getCustomerId(), + $this->customer->getId() + ); + + // Assert, that customer has increased coupon usage of specific rule + $this->assertEquals( + 1, + $ruleCustomer->getTimesUsed() + ); + } + + /** + * @magentoDataFixture Magento/Sales/_files/order.php + */ + public function testOrderCancelingDecreasesCouponUsages() + { + $this->processOrder($this->order); + + // Should not throw exception as bux is fixed now + $this->order->cancel(); + $ruleCustomer = $this->getSalesruleCustomerUsage($this->customer, $this->salesRule); + + // Assert, that rule customer model has been created for specific customer + $this->assertEquals( + $ruleCustomer->getCustomerId(), + $this->customer->getId() + ); + + // Assert, that customer has increased coupon usage of specific rule + $this->assertEquals( + 0, + $ruleCustomer->getTimesUsed() + ); + } + + /** + * @param Order $order + * @return \Magento\Sales\Api\Data\OrderInterface + */ + private function processOrder(Order $order) + { + $order->setState(Order::STATE_PROCESSING); + $order->setStatus(Order::STATE_PROCESSING); + return $this->orderRepository->save($order); + } + + /** + * @param Customer $customer + * @param Rule $rule + * @return Rule\Customer + */ + private function getSalesruleCustomerUsage(Customer $customer, Rule $rule) : \Magento\SalesRule\Model\Rule\Customer + { + $ruleCustomer = $this->ruleCustomerFactory->create(); + return $ruleCustomer->loadByCustomerRule($customer->getId(), $rule->getRuleId()); + } + + /** + * @return Rule + */ + private function prepareSalesRule() : Rule + { + /** @var Rule $salesRule */ + $salesRule = $this->objectManager->create(Rule::class); + $salesRule->setData( + [ + 'name' => '15$ fixed discount on whole cart', + 'is_active' => 1, + 'customer_group_ids' => [GroupManagement::NOT_LOGGED_IN_ID], + 'coupon_type' => Rule::COUPON_TYPE_SPECIFIC, + 'conditions' => [ + [ + 'type' => \Magento\SalesRule\Model\Rule\Condition\Address::class, + 'attribute' => 'base_subtotal', + 'operator' => '>', + 'value' => 45, + ], + ], + 'simple_action' => Rule::CART_FIXED_ACTION, + 'discount_amount' => 15, + 'discount_step' => 0, + 'stop_rules_processing' => 1, + 'website_ids' => [ + $this->objectManager->get(StoreManagerInterface::class)->getWebsite()->getId(), + ], + ] + ); + Bootstrap::getObjectManager()->get( + \Magento\SalesRule\Model\ResourceModel\Rule::class + )->save($salesRule); + + return $salesRule; + } + + /** + * @param Rule $salesRule + * @return Coupon + */ + private function attachSalesruleCoupon(Rule $salesRule) : Coupon + { + $coupon = $this->objectManager->create(Coupon::class); + $coupon->setRuleId($salesRule->getId()) + ->setCode('CART_FIXED_DISCOUNT_15') + ->setType(0); + + Bootstrap::getObjectManager()->get(CouponRepositoryInterface::class)->save($coupon); + + return $coupon; + } + + /** + * @param Coupon $coupon + * @return Order + */ + private function makeOrderWithCouponAsGuest(Coupon $coupon) : Order + { + $order = Bootstrap::getObjectManager()->create(\Magento\Sales\Model\Order::class); + $order->loadByIncrementId('100000001') + ->setCustomerIsGuest(true) + ->setCouponCode($coupon->getCode()) + ->setCreatedAt('2014-10-25 10:10:10') + ->setAppliedRuleIds($coupon->getRuleId()) + ->save(); + + return $order; + } + + /** + * @param Order $order + */ + private function delegateOrderToBeAssigned(Order $order) + { + $this->delegateCustomerService->delegateNew($order->getId()); + } + + /** + * @return Customer + * @throws \Magento\Framework\Exception\InputException + * @throws \Magento\Framework\Exception\LocalizedException + * @throws \Magento\Framework\Exception\State\InputMismatchException + */ + private function registerNewCustomer() : Customer + { + $customer = Bootstrap::getObjectManager()->create( + \Magento\Customer\Api\Data\CustomerInterface::class + ); + + /** @var Magento\Customer\Api\Data\CustomerInterface $customer */ + $customer->setWebsiteId(1) + ->setEmail('customer@example.com') + ->setGroupId(1) + ->setStoreId(1) + ->setPrefix('Mr.') + ->setFirstname('John') + ->setMiddlename('A') + ->setLastname('Smith') + ->setSuffix('Esq.') + ->setDefaultBilling(1) + ->setDefaultShipping(1) + ->setTaxvat('12') + ->setGender(0); + + $customer = $this->customerRepository->save($customer, 'password'); + + return $customer; + } +} diff --git a/dev/tests/integration/testsuite/Magento/Shipping/Controller/Adminhtml/Order/Shipment/AbstractShipmentControllerTest.php b/dev/tests/integration/testsuite/Magento/Shipping/Controller/Adminhtml/Order/Shipment/AbstractShipmentControllerTest.php new file mode 100644 index 0000000000000..0a1926d58624c --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Shipping/Controller/Adminhtml/Order/Shipment/AbstractShipmentControllerTest.php @@ -0,0 +1,92 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Shipping\Controller\Adminhtml\Order\Shipment; + +use Magento\Framework\Api\SearchCriteria; +use Magento\Framework\Api\SearchCriteriaBuilder; +use Magento\Framework\Data\Form\FormKey; +use Magento\Sales\Api\Data\OrderInterface; +use Magento\Sales\Api\Data\ShipmentInterface; +use Magento\Sales\Model\OrderRepository; +use Magento\TestFramework\Mail\Template\TransportBuilderMock; +use Magento\TestFramework\TestCase\AbstractBackendController; + +/** + * Abstract backend shipment test. + */ +class AbstractShipmentControllerTest extends AbstractBackendController +{ + /** + * @var TransportBuilderMock + */ + protected $transportBuilder; + + /** + * @var OrderRepository + */ + protected $orderRepository; + + /** + * @var FormKey + */ + protected $formKey; + + /** + * @var string + */ + protected $resource = 'Magento_Sales::shipment'; + + /** + * @inheritdoc + */ + protected function setUp() + { + parent::setUp(); + $this->transportBuilder = $this->_objectManager->get(TransportBuilderMock::class); + $this->orderRepository = $this->_objectManager->get(OrderRepository::class); + $this->formKey = $this->_objectManager->get(FormKey::class); + } + + /** + * @param string $incrementalId + * @return OrderInterface|null + */ + protected function getOrder(string $incrementalId) + { + /** @var SearchCriteria $searchCriteria */ + $searchCriteria = $this->_objectManager->create(SearchCriteriaBuilder::class) + ->addFilter(OrderInterface::INCREMENT_ID, $incrementalId) + ->create(); + + $orders = $this->orderRepository->getList($searchCriteria)->getItems(); + /** @var OrderInterface|null $order */ + $order = reset($orders); + + return $order; + } + + /** + * @param OrderInterface $order + * @return ShipmentInterface + */ + protected function getShipment(OrderInterface $order): ShipmentInterface + { + /** @var \Magento\Sales\Model\ResourceModel\Order\Shipment\Collection $shipmentCollection */ + $shipmentCollection = $this->_objectManager->create( + \Magento\Sales\Model\ResourceModel\Order\Shipment\CollectionFactory::class + )->create(); + + /** @var ShipmentInterface $shipment */ + $shipment = $shipmentCollection + ->setOrderFilter($order) + ->setPageSize(1) + ->getFirstItem(); + + return $shipment; + } +} diff --git a/dev/tests/integration/testsuite/Magento/Shipping/Controller/Adminhtml/Order/Shipment/AddCommentTest.php b/dev/tests/integration/testsuite/Magento/Shipping/Controller/Adminhtml/Order/Shipment/AddCommentTest.php new file mode 100644 index 0000000000000..c86ad71e7d5ca --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Shipping/Controller/Adminhtml/Order/Shipment/AddCommentTest.php @@ -0,0 +1,102 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Shipping\Controller\Adminhtml\Order\Shipment; + +use PHPUnit\Framework\Constraint\RegularExpression; +use PHPUnit\Framework\Constraint\StringContains; + +/** + * Class verifies shipment add comment functionality. + * + * @magentoDbIsolation enabled + * @magentoAppArea adminhtml + * @magentoDataFixture Magento/Sales/_files/shipment.php + */ +class AddCommentTest extends AbstractShipmentControllerTest +{ + /** + * @var string + */ + protected $uri = 'backend/admin/order_shipment/addComment'; + + /** + * @return void + */ + public function testSendEmailOnShipmentCommentAdd() + { + $comment = 'Test Shipment Comment'; + $order = $this->prepareRequest( + [ + 'comment' => ['comment' => $comment, 'is_customer_notified' => true], + ] + ); + $this->dispatch('backend/admin/order_shipment/addComment'); + $html = $this->getResponse()->getBody(); + $this->assertContains($comment, $html); + + $message = $this->transportBuilder->getSentMessage(); + $subject =__('Update to your %1 shipment', $order->getStore()->getFrontendName())->render(); + $messageConstraint = $this->logicalAnd( + new StringContains($order->getBillingAddress()->getName()), + new RegularExpression( + sprintf( + "/Your order #%s has been updated with a status of.*%s/", + $order->getIncrementId(), + $order->getFrontendStatusLabel() + ) + ), + new StringContains($comment) + ); + + $this->assertEquals($message->getSubject(), $subject); + $this->assertThat($message->getRawMessage(), $messageConstraint); + } + + /** + * @inheritdoc + */ + public function testAclHasAccess() + { + $this->prepareRequest(['comment', ['comment' => 'Comment']]); + + parent::testAclHasAccess(); + } + + /** + * @inheritdoc + */ + public function testAclNoAccess() + { + $this->prepareRequest(['comment', ['comment' => 'Comment']]); + + parent::testAclNoAccess(); + } + + /** + * @param array $params + * @return \Magento\Sales\Api\Data\OrderInterface|null + */ + private function prepareRequest(array $params = []) + { + $order = $this->getOrder('100000001'); + $shipment = $this->getShipment($order); + + $this->getRequest()->setMethod('POST'); + $this->getRequest()->setParams( + [ + 'id' => $shipment->getEntityId(), + 'form_key' => $this->formKey->getFormKey(), + ] + ); + + $data = $params ?? []; + $this->getRequest()->setPostValue($data); + + return $order; + } +} diff --git a/dev/tests/integration/testsuite/Magento/Shipping/Controller/Adminhtml/Order/Shipment/SaveTest.php b/dev/tests/integration/testsuite/Magento/Shipping/Controller/Adminhtml/Order/Shipment/SaveTest.php new file mode 100644 index 0000000000000..4eb65678583aa --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Shipping/Controller/Adminhtml/Order/Shipment/SaveTest.php @@ -0,0 +1,97 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Shipping\Controller\Adminhtml\Order\Shipment; + +use PHPUnit\Framework\Constraint\StringContains; + +/** + * Class verifies shipment creation functionality. + * + * @magentoDbIsolation enabled + * @magentoAppArea adminhtml + * @magentoDataFixture Magento/Sales/_files/order.php + */ +class SaveTest extends AbstractShipmentControllerTest +{ + /** + * @var string + */ + protected $uri = 'backend/admin/order_shipment/save'; + + /** + * @return void + */ + public function testSendEmailOnShipmentSave() + { + $order = $this->prepareRequest(['shipment' => ['send_email' => true]]); + $this->dispatch('backend/admin/order_shipment/save'); + + $this->assertSessionMessages( + $this->equalTo([(string)__('The shipment has been created.')]), + \Magento\Framework\Message\MessageInterface::TYPE_SUCCESS + ); + $this->assertRedirect($this->stringContains('sales/order/view/order_id/' . $order->getEntityId())); + + $shipment = $this->getShipment($order); + $message = $this->transportBuilder->getSentMessage(); + $subject = __('Your %1 order has shipped', $order->getStore()->getFrontendName())->render(); + $messageConstraint = $this->logicalAnd( + new StringContains($order->getBillingAddress()->getName()), + new StringContains( + 'Thank you for your order from ' . $shipment->getStore()->getFrontendName() + ), + new StringContains( + "Your Shipment #{$shipment->getIncrementId()} for Order #{$order->getIncrementId()}" + ) + ); + + $this->assertEquals($message->getSubject(), $subject); + $this->assertThat($message->getRawMessage(), $messageConstraint); + } + + /** + * @inheritdoc + */ + public function testAclHasAccess() + { + $this->prepareRequest(); + + parent::testAclHasAccess(); + } + + /** + * @inheritdoc + */ + public function testAclNoAccess() + { + $this->prepareRequest(); + + parent::testAclNoAccess(); + } + + /** + * @param array $params + * @return \Magento\Sales\Api\Data\OrderInterface|null + */ + private function prepareRequest(array $params = []) + { + $order = $this->getOrder('100000001'); + $this->getRequest()->setMethod('POST'); + $this->getRequest()->setParams( + [ + 'order_id' => $order->getEntityId(), + 'form_key' => $this->formKey->getFormKey(), + ] + ); + + $data = $params ?? []; + $this->getRequest()->setPostValue($data); + + return $order; + } +} diff --git a/dev/tests/integration/testsuite/Magento/Store/Model/StoreTest.php b/dev/tests/integration/testsuite/Magento/Store/Model/StoreTest.php index e62436460998f..9150ffb12b82d 100644 --- a/dev/tests/integration/testsuite/Magento/Store/Model/StoreTest.php +++ b/dev/tests/integration/testsuite/Magento/Store/Model/StoreTest.php @@ -4,13 +4,13 @@ * See COPYING.txt for license details. */ -// @codingStandardsIgnoreFile - namespace Magento\Store\Model; +use Magento\Catalog\Model\ProductRepository; use Magento\Framework\App\Bootstrap; use Magento\Framework\App\Filesystem\DirectoryList; use Magento\Framework\UrlInterface; +use Magento\Store\Api\StoreRepositoryInterface; use Zend\Stdlib\Parameters; /** @@ -200,7 +200,7 @@ public function testGetBaseUrlInPub() */ public function testGetBaseUrlForCustomEntryPoint($type, $useCustomEntryPoint, $useStoreCode, $expected) { - /* config operations require store to be loaded */ + /* config operations require store to be loaded */ $this->model->load('default'); \Magento\TestFramework\Helper\Bootstrap::getObjectManager() ->get(\Magento\Framework\App\Config\MutableScopeConfigInterface::class) @@ -269,12 +269,89 @@ public function testIsCanDelete() $this->assertFalse($this->model->isCanDelete()); } + /** + * @magentoDataFixture Magento/Store/_files/core_second_third_fixturestore.php + * @magentoDataFixture Magento/Catalog/_files/product_simple.php + * @magentoDbIsolation disabled + */ public function testGetCurrentUrl() { + $objectManager = \Magento\TestFramework\Helper\Bootstrap::getObjectManager(); + $objectManager->get(\Magento\Framework\App\Config\MutableScopeConfigInterface::class) + ->setValue('web/url/use_store', true, ScopeInterface::SCOPE_STORE, 'secondstore'); + $this->model->load('admin'); - $this->model->expects($this->any())->method('getUrl')->will($this->returnValue('http://localhost/index.php')); + $this->model + ->expects($this->any())->method('getUrl') + ->will($this->returnValue('http://localhost/index.php')); $this->assertStringEndsWith('default', $this->model->getCurrentUrl()); $this->assertStringEndsNotWith('default', $this->model->getCurrentUrl(false)); + + /** @var \Magento\Store\Model\Store $secondStore */ + $secondStore = $objectManager->get(StoreRepositoryInterface::class)->get('secondstore'); + + /** @var \Magento\Catalog\Model\ProductRepository $productRepository */ + $productRepository = $objectManager->create(ProductRepository::class); + $product = $productRepository->get('simple'); + $product->setStoreId($secondStore->getId()); + $url = $product->getUrlInStore(); + + $this->assertEquals( + $secondStore->getBaseUrl().'catalog/product/view/id/1/s/simple-product/', + $url + ); + $this->assertEquals( + $secondStore->getBaseUrl().'?___from_store=default', + $secondStore->getCurrentUrl() + ); + $this->assertEquals( + $secondStore->getBaseUrl(), + $secondStore->getCurrentUrl(false) + ); + } + + /** + * @magentoDataFixture Magento/Store/_files/second_store.php + * @magentoDataFixture Magento/Catalog/_files/category_product.php + * @magentoDbIsolation disabled + */ + public function testGetCurrentUrlWithUseStoreInUrlFalse() + { + $objectManager = \Magento\TestFramework\Helper\Bootstrap::getObjectManager(); + $objectManager->get(\Magento\Framework\App\Config\ReinitableConfigInterface::class) + ->setValue('web/url/use_store', false, ScopeInterface::SCOPE_STORE, 'default'); + + /** @var \Magento\Store\Model\Store $secondStore */ + $secondStore = $objectManager->get(StoreRepositoryInterface::class)->get('fixture_second_store'); + + /** @var \Magento\Catalog\Model\ProductRepository $productRepository */ + $productRepository = $objectManager->create(ProductRepository::class); + $product = $productRepository->get('simple333'); + + $product->setStoreId($secondStore->getId()); + $url = $product->getUrlInStore(); + + /** @var \Magento\Catalog\Model\CategoryRepository $categoryRepository */ + $categoryRepository = $objectManager->get(\Magento\Catalog\Model\CategoryRepository::class); + $category = $categoryRepository->get(333, $secondStore->getStoreId()); + + $this->assertEquals( + $secondStore->getBaseUrl().'catalog/category/view/s/category-1/id/333/', + $category->getUrl() + ); + $this->assertEquals( + $secondStore->getBaseUrl(). + 'catalog/product/view/id/333/s/simple-product-three/?___store=fixture_second_store', + $url + ); + $this->assertEquals( + $secondStore->getBaseUrl().'?___store=fixture_second_store&___from_store=default', + $secondStore->getCurrentUrl() + ); + $this->assertEquals( + $secondStore->getBaseUrl().'?___store=fixture_second_store', + $secondStore->getCurrentUrl(false) + ); } /** @@ -292,7 +369,11 @@ public function testCRUD() 'sort_order' => 0, 'is_active' => 1, ]); - $crud = new \Magento\TestFramework\Entity($this->model, ['name' => 'new name'], \Magento\Store\Model\Store::class); + $crud = new \Magento\TestFramework\Entity( + $this->model, + ['name' => 'new name'], + \Magento\Store\Model\Store::class + ); $crud->testCrud(); } diff --git a/dev/tests/static/testsuite/Magento/Test/Legacy/ObsoleteCodeTest.php b/dev/tests/static/testsuite/Magento/Test/Legacy/ObsoleteCodeTest.php index 8a00037d2513b..2a6079d619d4c 100644 --- a/dev/tests/static/testsuite/Magento/Test/Legacy/ObsoleteCodeTest.php +++ b/dev/tests/static/testsuite/Magento/Test/Legacy/ObsoleteCodeTest.php @@ -112,7 +112,7 @@ public function testPhpFiles() $changedFiles = ChangedFiles::getPhpFiles(__DIR__ . '/../_files/changed_files*'); $blacklistFiles = $this->getBlacklistFiles(); foreach ($blacklistFiles as $blacklistFile) { - unset($changedFiles[BP . $blacklistFile]); + unset($changedFiles[$blacklistFile]); } $invoker( function ($file) { @@ -920,7 +920,7 @@ private function processPattern($appPath, $pattern) $fileSet = glob($appPath . DIRECTORY_SEPARATOR . $pattern, GLOB_NOSORT); foreach ($fileSet as $file) { - $files[] = substr($file, $relativePathStart); + $files[] = ltrim(substr($file, $relativePathStart), '/'); } return $files; diff --git a/dev/tests/static/testsuite/Magento/Test/Less/_files/blacklist/old.txt b/dev/tests/static/testsuite/Magento/Test/Less/_files/blacklist/old.txt index 70764344de69b..7e29ef51964bc 100644 --- a/dev/tests/static/testsuite/Magento/Test/Less/_files/blacklist/old.txt +++ b/dev/tests/static/testsuite/Magento/Test/Less/_files/blacklist/old.txt @@ -4,6 +4,7 @@ app/design/adminhtml/Magento/backend/Magento_ConfigurableProduct/web/css/source/ app/design/adminhtml/Magento/backend/Magento_Developer/web/css/source/_module-old.less app/design/adminhtml/Magento/backend/Magento_Enterprise/web/css/source/_module-old.less app/design/adminhtml/Magento/backend/Magento_Msrp/web/css/source/_module-old.less +app/design/adminhtml/Magento/backend/Magento_Sales/web/css/source/module/_order.less app/design/adminhtml/Magento/backend/Magento_Tax/web/css/source/_module-old.less app/design/adminhtml/Magento/backend/Magento_Theme/web/css/source/_module-old.less app/design/adminhtml/Magento/backend/Magento_Ui/web/css/source/_module-old.less diff --git a/dev/tests/static/testsuite/Magento/Test/Php/_files/phpcpd/blacklist/common.txt b/dev/tests/static/testsuite/Magento/Test/Php/_files/phpcpd/blacklist/common.txt index 0ab8e0216c249..c3a66f0c33f0f 100644 --- a/dev/tests/static/testsuite/Magento/Test/Php/_files/phpcpd/blacklist/common.txt +++ b/dev/tests/static/testsuite/Magento/Test/Php/_files/phpcpd/blacklist/common.txt @@ -199,3 +199,5 @@ Magento/Framework/MessageQueue/Topology/Config/ExchangeConfigItem IntegrationConfig.php *Test.php setup/performance-toolkit/aggregate-report +Magento/Customer/Model/FileUploaderDataResolver.php +Magento/Customer/Model/Customer/DataProvider.php diff --git a/lib/internal/Magento/Framework/App/ScopeDefault.php b/lib/internal/Magento/Framework/App/ScopeDefault.php index 2ea62387145bf..e62d19f9ffbb4 100644 --- a/lib/internal/Magento/Framework/App/ScopeDefault.php +++ b/lib/internal/Magento/Framework/App/ScopeDefault.php @@ -17,7 +17,7 @@ class ScopeDefault implements ScopeInterface */ public function getCode() { - return 'default'; + return ''; } /** @@ -27,7 +27,7 @@ public function getCode() */ public function getId() { - return 1; + return 0; } /** diff --git a/lib/internal/Magento/Framework/Cache/Backend/Database.php b/lib/internal/Magento/Framework/Cache/Backend/Database.php index 291078383014a..231a8584cc8a5 100644 --- a/lib/internal/Magento/Framework/Cache/Backend/Database.php +++ b/lib/internal/Magento/Framework/Cache/Backend/Database.php @@ -27,11 +27,11 @@ * ) ENGINE=InnoDB DEFAULT CHARSET=utf8; */ -/** - * Database cache backend - */ namespace Magento\Framework\Cache\Backend; +/** + * Database cache backend. + */ class Database extends \Zend_Cache_Backend implements \Zend_Cache_Backend_ExtendedInterface { /** @@ -139,7 +139,7 @@ protected function _getTagsTable() * * Note : return value is always "string" (unserialization is done by the core not by the backend) * - * @param string $id Cache id + * @param string $id Cache id * @param boolean $doNotTestCacheValidity If set to true, the cache validity won't be tested * @return string|false cached datas */ @@ -432,7 +432,7 @@ public function touch($id, $extraLifetime) return $this->_getConnection()->update( $this->_getDataTable(), ['expire_time' => new \Zend_Db_Expr('expire_time+' . $extraLifetime)], - ['id=?' => $id, 'expire_time = 0 OR expire_time>' => time()] + ['id=?' => $id, 'expire_time = 0 OR expire_time>?' => time()] ); } else { return true; diff --git a/lib/internal/Magento/Framework/DB/Query/BatchRangeIterator.php b/lib/internal/Magento/Framework/DB/Query/BatchRangeIterator.php index cc2f5a91f73fd..af0ddc9b18b9f 100644 --- a/lib/internal/Magento/Framework/DB/Query/BatchRangeIterator.php +++ b/lib/internal/Magento/Framework/DB/Query/BatchRangeIterator.php @@ -107,7 +107,7 @@ public function __construct( public function current() { if (null === $this->currentSelect) { - $this->isValid = ($this->currentOffset + $this->batchSize) <= $this->totalItemCount; + $this->isValid = $this->currentOffset < $this->totalItemCount; $this->currentSelect = $this->initSelectObject(); } return $this->currentSelect; @@ -138,7 +138,7 @@ public function next() if (null === $this->currentSelect) { $this->current(); } - $this->isValid = ($this->batchSize + $this->currentOffset) <= $this->totalItemCount; + $this->isValid = $this->currentOffset < $this->totalItemCount; $select = $this->initSelectObject(); if ($this->isValid) { $this->iteration++; diff --git a/lib/internal/Magento/Framework/Data/Form/Element/Date.php b/lib/internal/Magento/Framework/Data/Form/Element/Date.php index 1920115ff0a3a..e762a641bfdcc 100644 --- a/lib/internal/Magento/Framework/Data/Form/Element/Date.php +++ b/lib/internal/Magento/Framework/Data/Form/Element/Date.php @@ -82,13 +82,13 @@ public function setValue($value) $this->_value = $value; return $this; } - if (preg_match('/^[0-9]+$/', $value)) { - $this->_value = (new \DateTime())->setTimestamp($this->_toTimestamp($value)); - return $this; - } - try { - $this->_value = new \DateTime($value, new \DateTimeZone($this->localeDate->getConfigTimezone())); + if (preg_match('/^[0-9]+$/', $value)) { + $this->_value = (new \DateTime())->setTimestamp($this->_toTimestamp($value)); + } else { + $this->_value = new \DateTime($value); + $this->_value->setTimezone(new \DateTimeZone($this->localeDate->getConfigTimezone())); + } } catch (\Exception $e) { $this->_value = ''; } diff --git a/lib/internal/Magento/Framework/Data/Form/Element/Editor.php b/lib/internal/Magento/Framework/Data/Form/Element/Editor.php index 6ed2d8293e61d..412fedaec3831 100644 --- a/lib/internal/Magento/Framework/Data/Form/Element/Editor.php +++ b/lib/internal/Magento/Framework/Data/Form/Element/Editor.php @@ -487,4 +487,13 @@ protected function isToggleButtonVisible() { return !$this->getConfig()->hasData('toggle_button') || $this->getConfig('toggle_button'); } + + /** + * @inheritdoc + */ + public function getHtmlId() + { + $suffix = $this->getConfig('dynamic_id') ? '${ $.wysiwygUniqueSuffix }' : ''; + return parent::getHtmlId() . $suffix; + } } diff --git a/lib/internal/Magento/Framework/Escaper.php b/lib/internal/Magento/Framework/Escaper.php index fff11dd7947a6..f3eefaed50d18 100644 --- a/lib/internal/Magento/Framework/Escaper.php +++ b/lib/internal/Magento/Framework/Escaper.php @@ -77,6 +77,7 @@ function ($errorNumber, $errorString) { throw new \Exception($errorString, $errorNumber); } ); + $data = $this->prepareUnescapedCharacters($data); $string = mb_convert_encoding($data, 'HTML-ENTITIES', 'UTF-8'); try { $domDocument->loadHTML( @@ -105,6 +106,19 @@ function ($errorNumber, $errorString) { return $result; } + /** + * Used to replace characters, that mb_convert_encoding will not process + * + * @param string $data + * @return string|null + */ + private function prepareUnescapedCharacters(string $data) + { + $patterns = ['/\&/u']; + $replacements = ['&']; + return \preg_replace($patterns, $replacements, $data); + } + /** * Remove not allowed tags * diff --git a/lib/internal/Magento/Framework/Event/Test/Unit/Config/_files/invalidEventsXmlArray.php b/lib/internal/Magento/Framework/Event/Test/Unit/Config/_files/invalidEventsXmlArray.php index 33007b7295bca..e0dc7494cca19 100644 --- a/lib/internal/Magento/Framework/Event/Test/Unit/Config/_files/invalidEventsXmlArray.php +++ b/lib/internal/Magento/Framework/Event/Test/Unit/Config/_files/invalidEventsXmlArray.php @@ -4,10 +4,6 @@ * See COPYING.txt for license details. */ return [ - 'without_event_handle' => [ - '<?xml version="1.0"?><config></config>', - ["Element 'config': Missing child element(s). Expected is ( event ).\nLine: 1\n"], - ], 'event_without_required_name_attribute' => [ '<?xml version="1.0"?><config><event name="some_name"></event></config>', ["Element 'event': Missing child element(s). Expected is ( observer ).\nLine: 1\n"], diff --git a/lib/internal/Magento/Framework/Event/etc/events.xsd b/lib/internal/Magento/Framework/Event/etc/events.xsd index d656b7fdb6ed6..cac62af356760 100644 --- a/lib/internal/Magento/Framework/Event/etc/events.xsd +++ b/lib/internal/Magento/Framework/Event/etc/events.xsd @@ -9,7 +9,7 @@ <xs:element name="config"> <xs:complexType> <xs:sequence> - <xs:element name="event" type="eventDeclaration" minOccurs="1" maxOccurs="unbounded"> + <xs:element name="event" type="eventDeclaration" minOccurs="0" maxOccurs="unbounded"> <xs:unique name="uniqueObserverName"> <xs:annotation> <xs:documentation> diff --git a/lib/internal/Magento/Framework/Mail/Template/TransportBuilder.php b/lib/internal/Magento/Framework/Mail/Template/TransportBuilder.php index a1c7333f41245..5e54a9441d1a1 100644 --- a/lib/internal/Magento/Framework/Mail/Template/TransportBuilder.php +++ b/lib/internal/Magento/Framework/Mail/Template/TransportBuilder.php @@ -48,6 +48,13 @@ class TransportBuilder */ protected $templateOptions; + /** + * Mail from address + * + * @var string|array + */ + private $from; + /** * Mail Transport * @@ -178,8 +185,7 @@ public function setReplyTo($email, $name = null) */ public function setFrom($from) { - $result = $this->_senderResolver->resolve($from); - $this->message->setFrom($result['email'], $result['name']); + $this->from = $from; return $this; } @@ -256,6 +262,7 @@ protected function reset() $this->templateIdentifier = null; $this->templateVars = null; $this->templateOptions = null; + $this->from = null; return $this; } @@ -289,6 +296,14 @@ protected function prepareMessage() ->setBody($body) ->setSubject(html_entity_decode($template->getSubject(), ENT_QUOTES)); + if ($this->from) { + $from = $this->_senderResolver->resolve( + $this->from, + $template->getDesignConfig()->getStore() + ); + $this->message->setFrom($from['email'], $from['name']); + } + return $this; } } diff --git a/lib/internal/Magento/Framework/Mail/Test/Unit/Template/TransportBuilderTest.php b/lib/internal/Magento/Framework/Mail/Test/Unit/Template/TransportBuilderTest.php index c3759bc43f81f..0bbfb37356caa 100644 --- a/lib/internal/Magento/Framework/Mail/Test/Unit/Template/TransportBuilderTest.php +++ b/lib/internal/Magento/Framework/Mail/Test/Unit/Template/TransportBuilderTest.php @@ -7,6 +7,7 @@ namespace Magento\Framework\Mail\Test\Unit\Template; use Magento\Framework\App\TemplateTypesInterface; +use Magento\Framework\DataObject; use Magento\Framework\Mail\MessageInterface; /** @@ -99,17 +100,37 @@ protected function setUp() */ public function testGetTransport($templateType, $messageType, $bodyText, $templateNamespace) { - $this->builder->setTemplateModel($templateNamespace); - $vars = ['reason' => 'Reason', 'customer' => 'Customer']; $options = ['area' => 'frontend', 'store' => 1]; + $from = 'email_from'; + $sender = ['email' => 'from@example.com', 'name' => 'name']; - $template = $this->createMock(\Magento\Framework\Mail\TemplateInterface::class); + $this->builder->setTemplateModel($templateNamespace); + $this->builder->setFrom($from); + + $template = $this->createPartialMock( + \Magento\Framework\Mail\TemplateInterface::class, + [ + 'setVars', + 'isPlain', + 'setOptions', + 'getSubject', + 'getType', + 'processTemplate', + 'getDesignConfig', + ] + ); $template->expects($this->once())->method('setVars')->with($this->equalTo($vars))->willReturnSelf(); $template->expects($this->once())->method('setOptions')->with($this->equalTo($options))->willReturnSelf(); $template->expects($this->once())->method('getSubject')->willReturn('Email Subject'); $template->expects($this->once())->method('getType')->willReturn($templateType); $template->expects($this->once())->method('processTemplate')->willReturn($bodyText); + $template->method('getDesignConfig')->willReturn(new DataObject($options)); + + $this->senderResolverMock->expects($this->once()) + ->method('resolve') + ->with($from, 1) + ->willReturn($sender); $this->templateFactoryMock->expects($this->once()) ->method('get') @@ -128,6 +149,9 @@ public function testGetTransport($templateType, $messageType, $bodyText, $templa ->method('setBody') ->with($this->equalTo($bodyText)) ->willReturnSelf(); + $this->messageMock->method('setFrom') + ->with($sender['email'], $sender['name']) + ->willReturnSelf(); $transport = $this->createMock(\Magento\Framework\Mail\TransportInterface::class); @@ -161,24 +185,6 @@ public function getTransportDataProvider() ]; } - /** - * @return void - */ - public function testSetFrom() - { - $sender = ['email' => 'from@example.com', 'name' => 'name']; - $this->senderResolverMock->expects($this->once()) - ->method('resolve') - ->with($sender) - ->willReturn($sender); - $this->messageMock->expects($this->once()) - ->method('setFrom') - ->with('from@example.com', 'name') - ->willReturnSelf(); - - $this->builder->setFrom($sender); - } - /** * @return void */ diff --git a/lib/internal/Magento/Framework/Reflection/DataObjectProcessor.php b/lib/internal/Magento/Framework/Reflection/DataObjectProcessor.php index 6311003bd2ad5..2f3caf08c534e 100644 --- a/lib/internal/Magento/Framework/Reflection/DataObjectProcessor.php +++ b/lib/internal/Magento/Framework/Reflection/DataObjectProcessor.php @@ -40,25 +40,33 @@ class DataObjectProcessor */ private $customAttributesProcessor; + /** + * @var array + */ + private $processors; + /** * @param MethodsMap $methodsMapProcessor * @param TypeCaster $typeCaster * @param FieldNamer $fieldNamer * @param CustomAttributesProcessor $customAttributesProcessor * @param ExtensionAttributesProcessor $extensionAttributesProcessor + * @param array $processors */ public function __construct( MethodsMap $methodsMapProcessor, TypeCaster $typeCaster, FieldNamer $fieldNamer, CustomAttributesProcessor $customAttributesProcessor, - ExtensionAttributesProcessor $extensionAttributesProcessor + ExtensionAttributesProcessor $extensionAttributesProcessor, + array $processors = [] ) { $this->methodsMapProcessor = $methodsMapProcessor; $this->typeCaster = $typeCaster; $this->fieldNamer = $fieldNamer; $this->extensionAttributesProcessor = $extensionAttributesProcessor; $this->customAttributesProcessor = $customAttributesProcessor; + $this->processors = $processors; } /** @@ -121,6 +129,27 @@ public function buildOutputDataArray($dataObject, $dataObjectType) $outputData[$key] = $value; } + + $outputData = $this->changeOutputArray($dataObject, $outputData); + + return $outputData; + } + + /** + * Change output array if needed. + * + * @param mixed $dataObject + * @param array $outputData + * @return array + */ + private function changeOutputArray($dataObject, array $outputData): array + { + foreach ($this->processors as $dataObjectClassName => $processor) { + if ($dataObject instanceof $dataObjectClassName) { + $outputData = $processor->execute($dataObject, $outputData); + } + } + return $outputData; } } diff --git a/lib/internal/Magento/Framework/Search/Adapter/Mysql/Query/Builder/Match.php b/lib/internal/Magento/Framework/Search/Adapter/Mysql/Query/Builder/Match.php index 8e3758817adf0..28e321d4c5d47 100644 --- a/lib/internal/Magento/Framework/Search/Adapter/Mysql/Query/Builder/Match.php +++ b/lib/internal/Magento/Framework/Search/Adapter/Mysql/Query/Builder/Match.php @@ -15,11 +15,16 @@ use Magento\Framework\Search\Adapter\Preprocessor\PreprocessorInterface; /** + * Class for building select where condition. + * * @api */ class Match implements QueryInterface { - const SPECIAL_CHARACTERS = '-+~/\\<>\'":*$#@()!,.?`=%&^'; + /** + * @var string + */ + const SPECIAL_CHARACTERS = '+~/\\<>\'":*$#@()!,.?`=%&^'; const MINIMAL_CHARACTER_LENGTH = 3; @@ -69,7 +74,7 @@ public function __construct( } /** - * {@inheritdoc} + * @inheritdoc */ public function build( ScoreBuilder $scoreBuilder, @@ -113,6 +118,8 @@ public function build( } /** + * Prepare query value for build function. + * * @param string $queryValue * @param string $conditionType * @return string diff --git a/lib/internal/Magento/Framework/Serialize/README.md b/lib/internal/Magento/Framework/Serialize/README.md index 5af8fb7f71b6b..d900f89208a54 100644 --- a/lib/internal/Magento/Framework/Serialize/README.md +++ b/lib/internal/Magento/Framework/Serialize/README.md @@ -3,6 +3,7 @@ **Serialize** library provides interface *SerializerInterface* and multiple implementations: * *Json* - default implementation. Uses PHP native json_encode/json_decode functions; + * *JsonHexTag* - default implementation. Uses PHP native json_encode/json_decode functions with `JSON_HEX_TAG` option enabled; * *Serialize* - less secure than *Json*, but gives higher performance on big arrays. Uses PHP native serialize/unserialize functions, does not unserialize objects on PHP 7. Using *Serialize* implementation directly is discouraged, always use *SerializerInterface*, using *Serialize* implementation may lead to security vulnerabilities. \ No newline at end of file diff --git a/lib/internal/Magento/Framework/Serialize/Serializer/Json.php b/lib/internal/Magento/Framework/Serialize/Serializer/Json.php index e352d0c2d7124..7ce9756ff243d 100644 --- a/lib/internal/Magento/Framework/Serialize/Serializer/Json.php +++ b/lib/internal/Magento/Framework/Serialize/Serializer/Json.php @@ -16,27 +16,27 @@ class Json implements SerializerInterface { /** - * {@inheritDoc} + * @inheritDoc * @since 100.2.0 */ public function serialize($data) { $result = json_encode($data); if (false === $result) { - throw new \InvalidArgumentException('Unable to serialize value.'); + throw new \InvalidArgumentException("Unable to serialize value. Error: " . json_last_error_msg()); } return $result; } /** - * {@inheritDoc} + * @inheritDoc * @since 100.2.0 */ public function unserialize($string) { $result = json_decode($string, true); if (json_last_error() !== JSON_ERROR_NONE) { - throw new \InvalidArgumentException('Unable to unserialize value.'); + throw new \InvalidArgumentException("Unable to unserialize value. Error: " . json_last_error_msg()); } return $result; } diff --git a/lib/internal/Magento/Framework/Serialize/Serializer/JsonHexTag.php b/lib/internal/Magento/Framework/Serialize/Serializer/JsonHexTag.php new file mode 100644 index 0000000000000..4a5406ff3fd99 --- /dev/null +++ b/lib/internal/Magento/Framework/Serialize/Serializer/JsonHexTag.php @@ -0,0 +1,35 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +declare(strict_types=1); + +namespace Magento\Framework\Serialize\Serializer; + +use Magento\Framework\Serialize\SerializerInterface; + +/** + * Serialize data to JSON with the JSON_HEX_TAG option enabled + * (All < and > are converted to \u003C and \u003E), + * unserialize JSON encoded data + * + * @api + * @since 100.2.0 + */ +class JsonHexTag extends Json implements SerializerInterface +{ + /** + * @inheritDoc + * @since 100.2.0 + */ + public function serialize($data): string + { + $result = json_encode($data, JSON_HEX_TAG); + if (false === $result) { + throw new \InvalidArgumentException('Unable to serialize value.'); + } + return $result; + } +} diff --git a/lib/internal/Magento/Framework/Serialize/Test/Unit/Serializer/JsonHexTagTest.php b/lib/internal/Magento/Framework/Serialize/Test/Unit/Serializer/JsonHexTagTest.php new file mode 100644 index 0000000000000..c867dced0fc6e --- /dev/null +++ b/lib/internal/Magento/Framework/Serialize/Test/Unit/Serializer/JsonHexTagTest.php @@ -0,0 +1,118 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +declare(strict_types=1); + +namespace Magento\Framework\Serialize\Test\Unit\Serializer; + +use Magento\Framework\DataObject; +use Magento\Framework\Serialize\Serializer\JsonHexTag; + +class JsonHexTagTest extends \PHPUnit\Framework\TestCase +{ + /** + * @var \Magento\Framework\Serialize\Serializer\Json + */ + private $json; + + protected function setUp() + { + $objectManager = new \Magento\Framework\TestFramework\Unit\Helper\ObjectManager($this); + $this->json = $objectManager->getObject(JsonHexTag::class); + } + + /** + * @param string|int|float|bool|array|null $value + * @param string $expected + * @dataProvider serializeDataProvider + */ + public function testSerialize($value, $expected) + { + $this->assertEquals( + $expected, + $this->json->serialize($value) + ); + } + + public function serializeDataProvider() + { + $dataObject = new DataObject(['something']); + return [ + ['', '""'], + ['string', '"string"'], + [null, 'null'], + [false, 'false'], + [['a' => 'b', 'd' => 123], '{"a":"b","d":123}'], + [123, '123'], + [10.56, '10.56'], + [$dataObject, '{}'], + ['< >', '"\u003C \u003E"'], + ]; + } + + /** + * @param string $value + * @param string|int|float|bool|array|null $expected + * @dataProvider unserializeDataProvider + */ + public function testUnserialize($value, $expected) + { + $this->assertEquals( + $expected, + $this->json->unserialize($value) + ); + } + + /** + * @return array + */ + public function unserializeDataProvider(): array + { + return [ + ['""', ''], + ['"string"', 'string'], + ['null', null], + ['false', false], + ['{"a":"b","d":123}', ['a' => 'b', 'd' => 123]], + ['123', 123], + ['10.56', 10.56], + ['{}', []], + ['"\u003C \u003E"', '< >'], + ]; + } + + /** + * @expectedException \InvalidArgumentException + * @expectedExceptionMessage Unable to serialize value. + */ + public function testSerializeException() + { + $this->json->serialize(STDOUT); + } + + /** + * @expectedException \InvalidArgumentException + * @expectedExceptionMessage Unable to unserialize value. + * @dataProvider unserializeExceptionDataProvider + */ + public function testUnserializeException($value) + { + $this->json->unserialize($value); + } + + /** + * @return array + */ + public function unserializeExceptionDataProvider(): array + { + return [ + [''], + [false], + [null], + ['{'] + ]; + } +} diff --git a/lib/internal/Magento/Framework/Test/Unit/DB/Query/BatchRangeIteratorTest.php b/lib/internal/Magento/Framework/Test/Unit/DB/Query/BatchRangeIteratorTest.php index 22fdf0a05686a..9e2014c1b070a 100644 --- a/lib/internal/Magento/Framework/Test/Unit/DB/Query/BatchRangeIteratorTest.php +++ b/lib/internal/Magento/Framework/Test/Unit/DB/Query/BatchRangeIteratorTest.php @@ -116,6 +116,6 @@ public function testIterations() $iterations++; } - $this->assertEquals(10, $iterations); + $this->assertEquals(11, $iterations); } } diff --git a/lib/internal/Magento/Framework/Test/Unit/EscaperTest.php b/lib/internal/Magento/Framework/Test/Unit/EscaperTest.php index ec05478b90db9..e406994b54c17 100644 --- a/lib/internal/Magento/Framework/Test/Unit/EscaperTest.php +++ b/lib/internal/Magento/Framework/Test/Unit/EscaperTest.php @@ -72,9 +72,9 @@ public function testEscapeJsEscapesOwaspRecommendedRanges() // Exceptions to escaping ranges $immune = [',', '.', '_']; for ($chr = 0; $chr < 0xFF; $chr++) { - if ($chr >= 0x30 && $chr <= 0x39 - || $chr >= 0x41 && $chr <= 0x5A - || $chr >= 0x61 && $chr <= 0x7A + if (($chr >= 0x30 && $chr <= 0x39) + || ($chr >= 0x41 && $chr <= 0x5A) + || ($chr >= 0x61 && $chr <= 0x7A) ) { $literal = $this->codepointToUtf8($chr); $this->assertEquals($literal, $this->escaper->escapeJs($literal)); @@ -174,6 +174,11 @@ public function escapeHtmlDataProvider() 'data' => '&<>"\'&<>"' ', 'expected' => '&<>"'&<>"' ' ], + 'text with special characters and allowed tag' => [ + 'data' => '&<br/>"\'&<>"' ', + 'expected' => '&<br>"'&<>"' ', + 'allowedTags' => ['br'], + ], 'text with multiple allowed tags, includes self closing tag' => [ 'data' => '<span>some text in tags<br /></span>', 'expected' => '<span>some text in tags<br></span>', diff --git a/lib/internal/Magento/Framework/View/Element/Html/Link/Current.php b/lib/internal/Magento/Framework/View/Element/Html/Link/Current.php index 43bfd46c1193a..7aac210dcab89 100644 --- a/lib/internal/Magento/Framework/View/Element/Html/Link/Current.php +++ b/lib/internal/Magento/Framework/View/Element/Html/Link/Current.php @@ -5,6 +5,10 @@ */ namespace Magento\Framework\View\Element\Html\Link; +use Magento\Framework\App\DefaultPathInterface; +use Magento\Framework\View\Element\Template; +use Magento\Framework\View\Element\Template\Context; + /** * Block representing link with two possible states. * "Current" state means link leads to URL equivalent to URL of currently displayed page. @@ -17,25 +21,25 @@ * @method null|bool getCurrent() * @method \Magento\Framework\View\Element\Html\Link\Current setCurrent(bool $value) */ -class Current extends \Magento\Framework\View\Element\Template +class Current extends Template { /** * Default path * - * @var \Magento\Framework\App\DefaultPathInterface + * @var DefaultPathInterface */ protected $_defaultPath; /** * Constructor * - * @param \Magento\Framework\View\Element\Template\Context $context - * @param \Magento\Framework\App\DefaultPathInterface $defaultPath + * @param Context $context + * @param DefaultPathInterface $defaultPath * @param array $data */ public function __construct( - \Magento\Framework\View\Element\Template\Context $context, - \Magento\Framework\App\DefaultPathInterface $defaultPath, + Context $context, + DefaultPathInterface $defaultPath, array $data = [] ) { parent::__construct($context, $data); @@ -56,18 +60,20 @@ public function getHref() * Get current mca * * @return string + * @SuppressWarnings(PHPMD.RequestAwareBlockMethod) */ private function getMca() { $routeParts = [ - 'module' => $this->_request->getModuleName(), - 'controller' => $this->_request->getControllerName(), - 'action' => $this->_request->getActionName(), + (string)$this->_request->getModuleName(), + (string)$this->_request->getControllerName(), + (string)$this->_request->getActionName(), ]; $parts = []; + $pathParts = explode('/', trim($this->_request->getPathInfo(), '/')); foreach ($routeParts as $key => $value) { - if (!empty($value) && $value != $this->_defaultPath->getPart($key)) { + if (isset($pathParts[$key]) && $pathParts[$key] === $value) { $parts[] = $value; } } diff --git a/lib/internal/Magento/Framework/View/Test/Unit/Element/Html/Link/CurrentTest.php b/lib/internal/Magento/Framework/View/Test/Unit/Element/Html/Link/CurrentTest.php index 909748722a081..7070ec9d48c11 100644 --- a/lib/internal/Magento/Framework/View/Test/Unit/Element/Html/Link/CurrentTest.php +++ b/lib/internal/Magento/Framework/View/Test/Unit/Element/Html/Link/CurrentTest.php @@ -17,11 +17,6 @@ class CurrentTest extends \PHPUnit\Framework\TestCase */ protected $_requestMock; - /** - * @var \PHPUnit_Framework_MockObject_MockObject - */ - protected $_defaultPathMock; - /** * @var \Magento\Framework\TestFramework\Unit\Helper\ObjectManager */ @@ -32,7 +27,6 @@ protected function setUp() $this->_objectManager = new \Magento\Framework\TestFramework\Unit\Helper\ObjectManager($this); $this->_urlBuilderMock = $this->createMock(\Magento\Framework\UrlInterface::class); $this->_requestMock = $this->createMock(\Magento\Framework\App\Request\Http::class); - $this->_defaultPathMock = $this->createMock(\Magento\Framework\App\DefaultPathInterface::class); } public function testGetUrl() @@ -60,29 +54,46 @@ public function testIsCurrentIfIsset() $this->assertTrue($link->isCurrent()); } + /** + * Test if the current url is the same as link path + * + * @return void + */ public function testIsCurrent() { - $path = 'test/path'; - $url = 'http://example.com/a/b'; - - $this->_requestMock->expects($this->once())->method('getModuleName')->will($this->returnValue('a')); - $this->_requestMock->expects($this->once())->method('getControllerName')->will($this->returnValue('b')); - $this->_requestMock->expects($this->once())->method('getActionName')->will($this->returnValue('d')); - $this->_defaultPathMock->expects($this->atLeastOnce())->method('getPart')->will($this->returnValue('d')); + $path = 'test/index'; + $url = 'http://example.com/test/index'; + + $this->_requestMock->expects($this->once()) + ->method('getPathInfo') + ->will($this->returnValue('/test/index/')); + $this->_requestMock->expects($this->once()) + ->method('getModuleName') + ->will($this->returnValue('test')); + $this->_requestMock->expects($this->once()) + ->method('getControllerName') + ->will($this->returnValue('index')); + $this->_requestMock->expects($this->once()) + ->method('getActionName') + ->will($this->returnValue('index')); + $this->_urlBuilderMock->expects($this->at(0)) + ->method('getUrl') + ->with($path) + ->will($this->returnValue($url)); + $this->_urlBuilderMock->expects($this->at(1)) + ->method('getUrl') + ->with('test/index') + ->will($this->returnValue($url)); - $this->_urlBuilderMock->expects($this->at(0))->method('getUrl')->with($path)->will($this->returnValue($url)); - $this->_urlBuilderMock->expects($this->at(1))->method('getUrl')->with('a/b')->will($this->returnValue($url)); - - $this->_requestMock->expects($this->once())->method('getControllerName')->will($this->returnValue('b')); /** @var \Magento\Framework\View\Element\Html\Link\Current $link */ $link = $this->_objectManager->getObject( \Magento\Framework\View\Element\Html\Link\Current::class, [ 'urlBuilder' => $this->_urlBuilderMock, - 'request' => $this->_requestMock, - 'defaultPath' => $this->_defaultPathMock + 'request' => $this->_requestMock ] ); + $link->setPath($path); $this->assertTrue($link->isCurrent()); } diff --git a/lib/internal/Magento/Framework/Webapi/Rest/Response/Renderer/Xml.php b/lib/internal/Magento/Framework/Webapi/Rest/Response/Renderer/Xml.php index b4cfc61611a93..f25cd219e3eae 100644 --- a/lib/internal/Magento/Framework/Webapi/Rest/Response/Renderer/Xml.php +++ b/lib/internal/Magento/Framework/Webapi/Rest/Response/Renderer/Xml.php @@ -7,6 +7,9 @@ */ namespace Magento\Framework\Webapi\Rest\Response\Renderer; +/** + * Renders response data in Xml format. + */ class Xml implements \Magento\Framework\Webapi\Rest\Response\RendererInterface { /** @@ -111,8 +114,7 @@ protected function _formatValue($value) /** Without the following transformation boolean values are rendered incorrectly */ $value = $value ? 'true' : 'false'; } - $replacementMap = ['&' => '&']; - return str_replace(array_keys($replacementMap), array_values($replacementMap), $value); + return (string) $value; } /** diff --git a/lib/internal/Magento/Framework/Webapi/Test/Unit/Rest/Response/Renderer/XmlTest.php b/lib/internal/Magento/Framework/Webapi/Test/Unit/Rest/Response/Renderer/XmlTest.php index 396fbcdb1978b..71fb41491cc74 100644 --- a/lib/internal/Magento/Framework/Webapi/Test/Unit/Rest/Response/Renderer/XmlTest.php +++ b/lib/internal/Magento/Framework/Webapi/Test/Unit/Rest/Response/Renderer/XmlTest.php @@ -76,6 +76,11 @@ public function providerXmlRender() '<?xml version="1.0"?><response><item_7key>value</item_7key></response>', 'Invalid XML render with numeric symbol in data index.' ], + [ + ['key' => 'test & foo'], + '<?xml version="1.0"?><response><key>test & foo</key></response>', + 'Invalid XML render with ampersand symbol in data index.' + ], [ ['.key' => 'value'], '<?xml version="1.0"?><response><item_key>value</item_key></response>', diff --git a/lib/web/css/docs/forms.html b/lib/web/css/docs/forms.html index 211a9bd650ba0..dc08ddffd2066 100644 --- a/lib/web/css/docs/forms.html +++ b/lib/web/css/docs/forms.html @@ -713,7 +713,7 @@ <h2 id="simple-form-with-required-fields-message">Simple form with "require <th>@_type</th> <td class="vars_value">@form-element-input-type</td> <td class="vars_value">'' [input-text | select | textarea | input-radio | input-checkbox]</td> - <td>Form control type.<br/><b>@form-element-input__[]</b> global variables are used to set up all form elements style. Control-specific global variables use these <b>@form-element-input__[]</b> variables by default. Control-specific global variables can be set up separately.<br/><b>@input-text__[]</b> is used to set up input-text controls style<br/><b>@select__[]</b> is used to set up selects style<br/><b>@textarea__[]</b> is used to set up textarea style</td> + <td>Form control type.<br/><strong>@form-element-input__[]</strong> global variables are used to set up all form elements style. Control-specific global variables use these <strong>@form-element-input__[]</strong> variables by default. Control-specific global variables can be set up separately.<br/><strong>@input-text__[]</strong> is used to set up input-text controls style<br/><strong>@select__[]</strong> is used to set up selects style<br/><strong>@textarea__[]</strong> is used to set up textarea style</td> </tr> <tr> <th>@_background</th> diff --git a/lib/web/css/docs/variables.html b/lib/web/css/docs/variables.html index 4f353dc1554a4..ebbf2122ab209 100644 --- a/lib/web/css/docs/variables.html +++ b/lib/web/css/docs/variables.html @@ -3507,7 +3507,7 @@ <h4 id="the-codelibformelementinputcoed-mixin-variables">The <code>.lib-form-ele <th>@_type</th> <td class="vars_value">@form-element-input-type</td> <td class="vars_value">'' [input-text | select | textarea | input-radio | input-checkbox]</td> - <td>Form control type.<br/><b>@form-element-input__[]</b> global variables are used to set up all form elements style. Control-specific global variables use these <b>@form-element-input__[]</b> variables by default. Control-specific global variables can be set up separately.<br/><b>@input-text__[]</b> is used to set up input-text controls style<br/><b>@select__[]</b> is used to set up selects style<br/><b>@textarea__[]</b> is used to set up textarea style</td> + <td>Form control type.<br/><strong>@form-element-input__[]</strong> global variables are used to set up all form elements style. Control-specific global variables use these <strong>@form-element-input__[]</strong> variables by default. Control-specific global variables can be set up separately.<br/><strong>@input-text__[]</strong> is used to set up input-text controls style<br/><strong>@select__[]</strong> is used to set up selects style<br/><strong>@textarea__[]</strong> is used to set up textarea style</td> </tr> <tr> <th>@_background</th> diff --git a/lib/web/css/source/lib/_forms.less b/lib/web/css/source/lib/_forms.less index b1c7a49da4a7a..5b6ff81bb4a65 100644 --- a/lib/web/css/source/lib/_forms.less +++ b/lib/web/css/source/lib/_forms.less @@ -300,6 +300,8 @@ input[type="checkbox"] { .lib-form-element-choice(@_type: input-checkbox); + position: relative; + top: 2px; } input[type="radio"] { diff --git a/lib/web/css/source/lib/_navigation.less b/lib/web/css/source/lib/_navigation.less index acae3c629500e..6232333fca33f 100644 --- a/lib/web/css/source/lib/_navigation.less +++ b/lib/web/css/source/lib/_navigation.less @@ -330,6 +330,19 @@ padding-right: 0; } + &:hover { + &:after { + content: ''; + display: block; + position: absolute; + top: 0; + left: 100%; + width: 10px; + height: calc(100% + 3px); + z-index: 1; + } + } + > .level-top { .lib-css(background, @_nav-level0-item-background-color); .lib-css(border, @_nav-level0-item-border); @@ -408,6 +421,17 @@ @_left: @_submenu-arrow-left ); + &:before { + content: ''; + display: block; + position: absolute; + width: 100%; + height: 4px; + left: 0; + top: -4px; + z-index: 1; + } + a { display: block; line-height: inherit; diff --git a/lib/web/fotorama/fotorama.js b/lib/web/fotorama/fotorama.js index 4062c501cb798..4f323e4312f6b 100644 --- a/lib/web/fotorama/fotorama.js +++ b/lib/web/fotorama/fotorama.js @@ -2481,11 +2481,11 @@ fotoramaVersion = '4.6.4'; .append($videoPlay.clone()); // This solves tabbing problems - addFocus(frame, function () { + addFocus(frame, function (e) { setTimeout(function () { lockScroll($stage); }, 0); - clickToShow({index: frameData.eq, user: true}); + clickToShow({index: frameData.eq, user: true}, e); }); $stageFrame = $stageFrame.add($frame); @@ -3034,7 +3034,10 @@ fotoramaVersion = '4.6.4'; return time; } - that.showStage = function (silent, options, time) { + that.showStage = function (silent, options, time, e) { + if (e !== undefined && e.target.tagName == 'IFRAME') { + return; + } unloadVideo($videoPlaying, activeFrame.i !== data[normalizeIndex(repositionIndex)].i); frameDraw(activeIndexes, 'stage'); stageFramePosition(SLOW ? [dirtyIndex] : [dirtyIndex, getPrevIndex(dirtyIndex), getNextIndex(dirtyIndex)]); @@ -3122,7 +3125,7 @@ fotoramaVersion = '4.6.4'; } }; - that.show = function (options) { + that.show = function (options, e) { that.longPress.singlePressInProgress = true; var index = calcActiveIndex(options); @@ -3133,7 +3136,7 @@ fotoramaVersion = '4.6.4'; var silent = _activeFrame === activeFrame && !options.user; - that.showStage(silent, options, time); + that.showStage(silent, options, time, e); that.showNav(silent, options, time); showedFLAG = typeof lastActiveIndex !== 'undefined' && lastActiveIndex !== activeIndex; @@ -3493,7 +3496,7 @@ fotoramaVersion = '4.6.4'; $stage.on('mousemove', stageCursor); - function clickToShow(showOptions) { + function clickToShow(showOptions, e) { clearTimeout(clickToShow.t); if (opts.clicktransition && opts.clicktransition !== opts.transition) { @@ -3510,7 +3513,7 @@ fotoramaVersion = '4.6.4'; }, 10); }, 0); } else { - that.show(showOptions); + that.show(showOptions, e); } } diff --git a/lib/web/mage/adminhtml/wysiwyg/tiny_mce/html5-schema.js b/lib/web/mage/adminhtml/wysiwyg/tiny_mce/html5-schema.js index 98ce6a005db04..41def246989db 100644 --- a/lib/web/mage/adminhtml/wysiwyg/tiny_mce/html5-schema.js +++ b/lib/web/mage/adminhtml/wysiwyg/tiny_mce/html5-schema.js @@ -149,7 +149,8 @@ define([ ['col', 'width align char charoff valign'], ['input button select textarea', 'autofocus'], ['input textarea', 'placeholder onselect onchange onfocus onblur'], - ['link script img', 'crossorigin'] + ['link script img', 'crossorigin'], + ['div','aria-role'] ]; rawData.forEach(function (data) { diff --git a/lib/web/mage/dataPost.js b/lib/web/mage/dataPost.js index 5d052f12db8fb..cc56ee266e08a 100644 --- a/lib/web/mage/dataPost.js +++ b/lib/web/mage/dataPost.js @@ -57,7 +57,7 @@ define([ */ postData: function (params) { var formKey = $(this.options.formKeyInputSelector).val(), - $form; + $form, input; if (formKey) { params.data['form_key'] = formKey; @@ -67,6 +67,19 @@ define([ data: params })); + if (params.files) { + $form[0].enctype = 'multipart/form-data'; + $.each(params.files, function (key, files) { + if (files instanceof FileList) { + input = document.createElement('input'); + input.type = 'file'; + input.name = key; + input.files = files; + $form[0].appendChild(input); + } + }); + } + if (params.data.confirmation) { uiConfirm({ content: params.data.confirmationMessage, diff --git a/lib/web/mage/gallery/gallery.js b/lib/web/mage/gallery/gallery.js index db98a1cc39a30..15c3d01cf2be3 100644 --- a/lib/web/mage/gallery/gallery.js +++ b/lib/web/mage/gallery/gallery.js @@ -472,8 +472,18 @@ define([ * @param {Array.<Object>} data - Set of gallery items to update. */ updateData: function (data) { + var mainImageIndex; + if (_.isArray(data)) { settings.fotoramaApi.load(data); + mainImageIndex = getMainImageIndex(data); + + if (mainImageIndex) { + settings.fotoramaApi.show({ + index: mainImageIndex, + time: 0 + }); + } $.extend(false, settings, { data: data, diff --git a/lib/web/mage/gallery/gallery.less b/lib/web/mage/gallery/gallery.less index f551157210692..fb95d3d1ed98c 100644 --- a/lib/web/mage/gallery/gallery.less +++ b/lib/web/mage/gallery/gallery.less @@ -768,6 +768,7 @@ max-width: inherit; position: absolute; top: 0; + object-fit: scale-down; } } diff --git a/lib/web/mage/menu.js b/lib/web/mage/menu.js index e4ccbdf325ecc..7533edd57b4a6 100644 --- a/lib/web/mage/menu.js +++ b/lib/web/mage/menu.js @@ -21,7 +21,7 @@ define([ expanded: false, showDelay: 42, hideDelay: 300, - delay: 300, + delay: 0, mediaBreakpoint: '(max-width: 768px)' }, @@ -439,6 +439,10 @@ define([ event.preventDefault(); target = $(event.target).closest('.ui-menu-item'); + if (target.length) { + target.get(0).scrollIntoView(); + } + if (!target.hasClass('level-top') || !target.has('.ui-menu').length) { window.location.href = target.find('> a').attr('href'); } diff --git a/lib/web/mage/validation.js b/lib/web/mage/validation.js index a9b5e736eb972..a742b8e6bbb27 100644 --- a/lib/web/mage/validation.js +++ b/lib/web/mage/validation.js @@ -1126,9 +1126,9 @@ ovId; if (!result) { - ovId = $(elm).attr('id') + '_value'; + ovId = $('#' + $(elm).attr('id') + '_value'); - if ($(ovId)) { + if (ovId.length > 0) { result = !$.mage.isEmptyNoTrim($(ovId).val()); } } @@ -1944,6 +1944,9 @@ } if (firstActive.length) { + $('html, body').animate({ + scrollTop: firstActive.offset().top + }); firstActive.focus(); } } diff --git a/lib/web/magnifier/magnifier.js b/lib/web/magnifier/magnifier.js index 0807a4c394995..f6ea1300e77ea 100644 --- a/lib/web/magnifier/magnifier.js +++ b/lib/web/magnifier/magnifier.js @@ -542,7 +542,11 @@ showWrapper = true; bindEvents(eventType, thumb); data[idx].status = 2; - data[idx].zoom = largeObj.height / largeWrapper.height(); + if (largeObj.width > largeObj.height) { + data[idx].zoom = largeObj.width / largeWrapper.width(); + } else { + data[idx].zoom = largeObj.height / largeWrapper.height(); + } setThumbData(thumb, data[idx]); updateLensOnLoad(idx, thumb, largeObj, largeWrapper); } diff --git a/setup/performance-toolkit/benchmark.jmx b/setup/performance-toolkit/benchmark.jmx index 80abe6da87f3c..64ff3bc9cba60 100644 --- a/setup/performance-toolkit/benchmark.jmx +++ b/setup/performance-toolkit/benchmark.jmx @@ -25803,6 +25803,39 @@ catch (java.lang.Exception e) { </ResponseAssertion> <hashTree/> </hashTree> + <HTTPSamplerProxy guiclass="HttpTestSampleGui" testclass="HTTPSamplerProxy" testname="Filled Order Page" enabled="true"> + <elementProp name="HTTPsampler.Arguments" elementType="Arguments" guiclass="HTTPArgumentsPanel" testclass="Arguments" enabled="true"> + <collectionProp name="Arguments.arguments"/> + </elementProp> + <stringProp name="HTTPSampler.domain"/> + <stringProp name="HTTPSampler.port"/> + <stringProp name="HTTPSampler.connect_timeout">60000</stringProp> + <stringProp name="HTTPSampler.response_timeout">200000</stringProp> + <stringProp name="HTTPSampler.protocol">${request_protocol}</stringProp> + <stringProp name="HTTPSampler.contentEncoding"/> + <stringProp name="HTTPSampler.path">${base_path}${admin_path}/sales/order_create/index/</stringProp> + <stringProp name="HTTPSampler.method">GET</stringProp> + <boolProp name="HTTPSampler.follow_redirects">true</boolProp> + <boolProp name="HTTPSampler.auto_redirects">false</boolProp> + <boolProp name="HTTPSampler.use_keepalive">true</boolProp> + <boolProp name="HTTPSampler.DO_MULTIPART_POST">false</boolProp> + <boolProp name="HTTPSampler.monitor">false</boolProp> + <stringProp name="HTTPSampler.embedded_url_re"/> + <stringProp name="TestPlan.comments">Detected the start of a redirect chain</stringProp> + </HTTPSamplerProxy> + <hashTree> + <ResponseAssertion guiclass="AssertionGui" testclass="ResponseAssertion" testname="Assert Filled Order Page" enabled="true"> + <collectionProp name="Asserion.test_strings"> + <stringProp name="-37823069">Select from existing customer addresses</stringProp> + <stringProp name="-13185722">Submit Order</stringProp> + <stringProp name="-209419315">Items Ordered</stringProp> + </collectionProp> + <stringProp name="Assertion.test_field">Assertion.response_data</stringProp> + <boolProp name="Assertion.assume_success">false</boolProp> + <intProp name="Assertion.test_type">2</intProp> + </ResponseAssertion> + <hashTree/> + </hashTree> <HTTPSamplerProxy guiclass="HttpTestSampleGui" testclass="HTTPSamplerProxy" testname="Save Order" enabled="true"> <elementProp name="HTTPsampler.Arguments" elementType="Arguments" guiclass="HTTPArgumentsPanel" testclass="Arguments" enabled="true"> <collectionProp name="Arguments.arguments"> diff --git a/setup/view/magento/setup/data-option.phtml b/setup/view/magento/setup/data-option.phtml index cb31d96013f24..656b04876c414 100644 --- a/setup/view/magento/setup/data-option.phtml +++ b/setup/view/magento/setup/data-option.phtml @@ -43,7 +43,7 @@ value="false" > <label class="form-label" for="keepData"> - <b>Keep data</b> + <strong>Keep data</strong> <br/><br/> Note: You can see the associated data in your database, however your customer will not see this in your online store. @@ -59,7 +59,7 @@ value="true" > <label class="form-label" for="removeData"> - <b>Remove data</b> + <strong>Remove data</strong> <br/><br/> Did you back up first? Choosing to remove data means the data is unavailable unless you recover it from a backup in the preceding step. diff --git a/setup/view/magento/setup/extension-grid.phtml b/setup/view/magento/setup/extension-grid.phtml index 47e5f7eec309b..4800d45e12d4f 100644 --- a/setup/view/magento/setup/extension-grid.phtml +++ b/setup/view/magento/setup/extension-grid.phtml @@ -3,6 +3,9 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ + +// @codingStandardsIgnoreFile + ?> <h2 class="page-title">{{$state.current.header}}</h2> @@ -66,8 +69,8 @@ <div class="row" ng-show="$root.isMarketplaceAuthorized && extensionsProcessed && total == 0"> <label class="form-label not-found"> - <b>You haven't purchased any extensions yet. Visit <a href="https://marketplace.magento.com">Marketplace</a> - for purchasing extensions.</b><br/> + <strong>You haven't purchased any extensions yet. Visit <a href="https://marketplace.magento.com">Marketplace</a> + for purchasing extensions.</strong><br/> </label> </div> <div class="admin__data-grid-outer-wrap" diff --git a/setup/view/magento/setup/module-grid.phtml b/setup/view/magento/setup/module-grid.phtml index 5fe0447b7c791..3bf9377104df1 100644 --- a/setup/view/magento/setup/module-grid.phtml +++ b/setup/view/magento/setup/module-grid.phtml @@ -8,7 +8,7 @@ <div class="row" ng-show="modulesProcessed && total == 0"> <label class="form-label"> - <b>We didn't find any modules at this time. Please try later.</b><br/> + <strong>We didn't find any modules at this time. Please try later.</strong><br/> </label> </div> <div class="admin__data-grid-outer-wrap" ng-show="modulesProcessed && total > 0"> diff --git a/setup/view/magento/setup/select-version.phtml b/setup/view/magento/setup/select-version.phtml index 920ab86252514..66264e46d50ad 100644 --- a/setup/view/magento/setup/select-version.phtml +++ b/setup/view/magento/setup/select-version.phtml @@ -128,7 +128,7 @@ </div> <div class="row" ng-show="componentsProcessed && total == 0"> <label class="form-label"> - <b>We didn't find any components to upgrade. Click "Next" to continue.<br/> + <strong>We didn't find any components to upgrade. Click "Next" to continue.</strong><br/> </label> </div> <div class="admin__data-grid-outer-wrap" ng-show="componentsProcessed && total > 0">